From 712072209ab5e3b3a14ebc0c770b020afe544560 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 30 Jun 2026 03:57:52 -0400 Subject: feat: all admin mapping features working --- internal/admin/static/map.js | 263 +++++++++++++++++++++++++++---------------- 1 file changed, 166 insertions(+), 97 deletions(-) (limited to 'internal/admin/static/map.js') diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index ca454a0..cd53bed 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -127,23 +127,22 @@ function renderMap(data) { var txtColor = hexLuminance(fill) > 0.35 ? '#111' : '#fff'; var sel = selectedRoom === r.id; roomHTML += ''; + var dimColor = hexLuminance(fill) > 0.35 ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.4)'; - roomHTML += '#'+r.id+''; - roomHTML += wrapText(r.name, x+w/2, y+h/2, w, txtColor); + roomHTML += '#'+r.id+''; + roomHTML += wrapText(r.name, x+w/2, y+h/2+2, w, txtColor); - var btnY = y+h-4; - var topY = y+4; if (hasUp[r.id] && upTarget[r.id]) { - roomHTML += '\u2191'; + roomHTML += '\u2191'; } if (hasDown[r.id] && downTarget[r.id]) { - roomHTML += '\u2193'; + roomHTML += '\u2193'; } if (sel) { - var btnX = x + 6, btnBW = 18; - roomHTML += '+\u2191'; - roomHTML += '+\u2193'; + var btnBW = 18; + roomHTML += '+\u2191'; + roomHTML += '+\u2193'; } }); } @@ -163,8 +162,10 @@ function renderMap(data) { var innerHTML = g + roomHTML + ghostHTML; var vb = [(-panX - W/2)/scale, (-panY - H/2)/scale, W/scale, H/scale]; - var svgHTML = ''+innerHTML+''; - svg.innerHTML = svgHTML; + svg.setAttribute('viewBox', vb.join(' ')); + svg.setAttribute('width', W); + svg.setAttribute('height', H); + svg.innerHTML = '' + innerHTML + ''; svg.addEventListener('contextmenu', function(e) { e.preventDefault(); }); @@ -292,18 +293,6 @@ function renderMap(data) { updateView(); }; - svg.querySelectorAll('rect.rm').forEach(function(r) { - r.addEventListener('mousemove', function(ev) { - var tip = $('#tooltip'); - tip.style.display = 'block'; - tip.style.left = (ev.clientX+12)+'px'; - tip.style.top = (ev.clientY+12)+'px'; - tip.textContent = '#'+r.getAttribute('data-id')+' '+r.getAttribute('data-name'); - }); - r.addEventListener('mouseleave', function() { - $('#tooltip').style.display = 'none'; - }); - }); } function wrapText(name, cx, cy, maxWidth, color) { @@ -311,10 +300,26 @@ function wrapText(name, cx, cy, maxWidth, color) { color = color || '#fff'; var charW = 6; var maxLen = Math.max(4, Math.floor((maxWidth - 4) / charW)); + var words = name.split(/\s+/); var lines = []; - for (var i = 0; i < name.length; i += maxLen) { - lines.push(name.substring(i, i + maxLen)); - } + var cur = ''; + words.forEach(function(w) { + if (w.length > maxLen) { + if (cur) { lines.push(cur); cur = ''; } + for (var j = 0; j < w.length; j += maxLen) { + lines.push(w.substring(j, j + maxLen)); + } + } else if (!cur) { + cur = w; + } else if ((cur + ' ' + w).length <= maxLen) { + cur += ' ' + w; + } else { + lines.push(cur); + cur = w; + } + }); + if (cur) lines.push(cur); + if (lines.length === 0) lines.push(''); if (lines.length > 3) lines = lines.slice(0, 3); var dy = -(lines.length - 1) * 6; return lines.map(function(l, i) { @@ -323,7 +328,7 @@ function wrapText(name, cx, cy, maxWidth, color) { } function updateView() { - var svg = document.querySelector('#mapSvg svg'); + var svg = $('#mapSvg'); if (!svg) return; var W = svg.parentElement.clientWidth; var H = svg.parentElement.clientHeight; @@ -332,6 +337,7 @@ function updateView() { } function selectRoom(id) { + if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } selectedRoom = id; loadMap(); API.get('/api/rooms/' + id).then(function(data) { @@ -361,18 +367,22 @@ function renderSidePanel(data) { var html = '
'; html += ''; - html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; html += '
'; html += '
'; if (currentPanelTab === 0) html += renderRoomTab(r, file); - else if (currentPanelTab === 1) html += renderLocalTab(localObjs); - else if (currentPanelTab === 2) html += renderObjectsTab(refObjs); - else if (currentPanelTab === 3) html += renderMobsTab(mobs); - else if (currentPanelTab === 4) html += renderItemsTab(spawns); + else if (currentPanelTab === 1) html += renderExitsTab(r); + else if (currentPanelTab === 2) html += renderLocalTab(localObjs); + else if (currentPanelTab === 3) html += renderObjectsTab(refObjs); + else if (currentPanelTab === 4) html += renderMobsTab(mobs); + else if (currentPanelTab === 5) html += renderItemsTab(spawns); + else if (currentPanelTab === 6) html += renderHazardTab(r); html += '
'; panel.innerHTML = html; @@ -401,23 +411,61 @@ function renderSidePanel(data) { function renderRoomTab(r, file) { var html = '
' + esc(file) + '
'; - html += '
'; - html += '
'; - html += '
'; - html += '
'; + html += '
'; + html += '
'; + html += '
'; var descText = ''; if (Array.isArray(r.description)) { descText = r.description.map(function(d) { return d.text || ''; }).join('\n\n'); } else if (typeof r.description === 'string') { descText = r.description; } - html += '
'; - html += '
'; - html += '
'; - html += renderExitsSection(r); + html += '
'; + html += '
'; + html += '
'; + return html; +} + +function renderExitsTab(r) { + return renderExitsSection(r); +} + +function renderHazardTab(r) { + var html = ''; + var current = r.hazard || ''; + if (current) { + html += '
' + esc(current) + '
'; + } return html; } +function searchHazards(input) { + searchAndShow(input, 'hazards', function(r) { + addHazard(r.id); + }); +} +function addHazard(id) { + if (!currentRoomData || !currentRoomData.room) return; + currentRoomData.room.hazard = id; + saveNow(); + reRenderPanel(); +} +function clearHazard() { + if (!currentRoomData || !currentRoomData.room) return; + currentRoomData.room.hazard = ''; + saveNow(); + reRenderPanel(); +} + +function updateRoomBlockTransport() { + if (!currentRoomData || !currentRoomData.room) return; + currentRoomData.room.block_transport = $('#roomBlockTransport').checked; + saveNow(); +} + function getRoomDirFromFile(file) { var rel = (file || '').replace(/^data\/rooms\/?/, ''); var idx = rel.indexOf('/'); @@ -441,10 +489,12 @@ function switchPanelTab(idx) { var mobs = Array.isArray(r.mobs) ? r.mobs : []; var html = ''; if (idx === 0) html = renderRoomTab(r, file); - else if (idx === 1) html = renderLocalTab(localObjs); - else if (idx === 2) html = renderObjectsTab(refObjs); - else if (idx === 3) html = renderMobsTab(mobs); - else if (idx === 4) html = renderItemsTab(spawns); + else if (idx === 1) html = renderExitsTab(r); + else if (idx === 2) html = renderLocalTab(localObjs); + else if (idx === 3) html = renderObjectsTab(refObjs); + else if (idx === 4) html = renderMobsTab(mobs); + else if (idx === 5) html = renderItemsTab(spawns); + else if (idx === 6) html = renderHazardTab(r); var content = document.querySelector('.panel-tab-content'); if (content) content.innerHTML = html; var tabs = document.querySelectorAll('.panel-tab'); @@ -482,7 +532,7 @@ function renderLocalTab(localObjs) { h += '
'; h += '
'; h += ''; - h += ''; + h += ''; h += ''; h += '
'; h += ''; @@ -557,8 +607,9 @@ function renderExitsSection(r) { var bmsg = (typeof exit === 'object' && exit.blocked_message) ? exit.blocked_message : ''; var setFlags = (typeof exit === 'object' && exit.set_flags) ? exit.set_flags : null; var setPlayerFlags = (typeof exit === 'object' && exit.set_player_flags) ? exit.set_player_flags : null; + var isConditional = !!(cond || bmsg || setFlags || setPlayerFlags); h += '
'; - h += '' + d + ''; + h += '' + d + ''; h += '# ' + target + ''; if (bmsg) h += 'Blocked: ' + esc(bmsg) + ''; if (cond) h += 'Conditional'; @@ -601,12 +652,12 @@ function editExitCondition(dir) { overlay.className = 'cm-overlay exit-overlay'; var menu = document.createElement('div'); menu.className = 'cm-menu exit-modal'; - menu.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);min-width:420px;max-height:85vh;overflow-y:auto;padding:14px;z-index:201'; + menu.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);min-width:420px;max-width:90vw;max-height:85vh;overflow-y:auto;overflow-x:hidden;padding:14px;z-index:201'; var h = '

Exit: ' + esc(dir) + ' → #' + exit.room + '

'; - h += '
'; h += '
'; h += '
'; + h += '
'; h += '
'; h += ''; @@ -857,7 +908,7 @@ function getRoomObjects() { function setRoomObjects(objs) { if (!currentRoomData || !currentRoomData.room) return; currentRoomData.room.objects = objs; - autoSave(); + saveNow(); } function getRoomSpawns() { @@ -870,7 +921,7 @@ function getRoomSpawns() { function setRoomSpawns(spawns) { if (!currentRoomData || !currentRoomData.room) return; currentRoomData.room.item_spawns = spawns; - autoSave(); + saveNow(); } function getRoomMobs() { @@ -883,7 +934,7 @@ function getRoomMobs() { function setRoomMobs(mobs) { if (!currentRoomData || !currentRoomData.room) return; currentRoomData.room.mobs = mobs; - autoSave(); + saveNow(); } function addLocalObject() { @@ -1012,6 +1063,7 @@ function searchAndShow(input, type, onSelect) { if (type === 'objects' && data.objects) items = data.objects; if (type === 'items' && data.items) items = data.items; if (type === 'mobs' && data.mobs) items = data.mobs; + if (type === 'hazards' && data.hazards) items = data.hazards; if (items.length === 0) { results.style.display = 'block'; results.innerHTML = '
No results
'; @@ -1054,12 +1106,20 @@ function autoSave() { saveTimer = setTimeout(function() { doSavePanel(); }, 600); } +function saveNow() { + if (saveTimer) clearTimeout(saveTimer); + saveTimer = null; + doSavePanel(); +} + function doSavePanel() { var id = selectedRoom; if (!id) return; var save = function(r) { API.put('/api/rooms/' + id, r).then(function() { + notify('Room #' + id + ' saved', 'success'); updateUndoBar(); + loadMap(); }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); }; @@ -1069,7 +1129,6 @@ function doSavePanel() { var el = $('#roomName'); if (el) r.name = el.value; el = $('#roomColor'); if (el) r.color = el.value; el = $('#roomDesc'); if (el && el.value) r.description = [{text: el.value}]; - el = $('#roomHazard'); if (el) r.hazard = el.value; el = $('#roomBlockTransport'); if (el) r.block_transport = el.checked; if (currentRoomData && currentRoomData.room) { @@ -1077,13 +1136,16 @@ function doSavePanel() { r.item_spawns = currentRoomData.room.item_spawns; r.mobs = currentRoomData.room.mobs; r.exits = currentRoomData.room.exits; + r.hazard = currentRoomData.room.hazard || ''; + if (r.block_transport === undefined) r.block_transport = currentRoomData.room.block_transport; } - var newID = parseInt($('#roomID').value) || id; - var newDir = $('#roomDir').value; + var el = $('#roomID'); var newID = el ? parseInt(el.value) || id : id; + var dirEl = $('#roomDir'); + var newDir = dirEl ? dirEl.value : null; var currentDir = (data.file || '').replace(/^data\/rooms\/?/, '').split('/')[0] || ''; - if (newDir !== currentDir && newDir !== undefined) { + if (dirEl && newDir !== currentDir) { API.post('/api/rooms/move', {id: id, dir: newDir}).then(function() { if (newID !== id) { API.post('/api/rooms/rename', {id: id, new_id: newID}).then(function() { @@ -1094,9 +1156,13 @@ function doSavePanel() { } else if (newID !== id) { API.post('/api/rooms/rename', {id: id, new_id: newID}).then(function() { selectRoom(newID); - }).catch(function(e) { notify('Rename failed: '+e.message, 'error'); save(r); }); - } else { save(r); } - }); + }).catch(function(e) { + notify('Rename failed: '+e.message, 'error'); + var rid = $('#roomID'); if (rid) rid.value = id; + save(r); + }); + } else { save(r); } + }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); } function deleteRoom(id) { @@ -1190,7 +1256,7 @@ function showRoomContextMenu(x, y, id) { } function mouseToGrid(e) { - var svgEl = document.querySelector('#mapSvg svg'); + var svgEl = $('#mapSvg'); if (!svgEl) return null; var rect = svgEl.getBoundingClientRect(); var mx = e.clientX - rect.left; @@ -1257,7 +1323,7 @@ function applyGhostLinkHighlight(fromRoom, gx, gy) { fromEl.setAttribute('stroke', '#0ff'); fromEl.setAttribute('stroke-width', '3'); - var g = document.querySelector('#mapSvg svg g'); + var g = document.querySelector('#mapSvg g'); if (!g) return; var svgNS = 'http://www.w3.org/2000/svg'; var line = document.createElementNS(svgNS, 'line'); @@ -1287,7 +1353,7 @@ function applyLinkHighlight(fromId, toId) { toEl.setAttribute('stroke', '#f0f'); toEl.setAttribute('stroke-width', '3'); - var g = document.querySelector('#mapSvg svg g'); + var g = document.querySelector('#mapSvg g'); if (!g) return; var svgNS = 'http://www.w3.org/2000/svg'; var line = document.createElementNS(svgNS, 'line'); @@ -1375,7 +1441,7 @@ function applyDelHighlight(fromId, toId) { toEl.setAttribute('stroke', '#f55'); toEl.setAttribute('stroke-width', '3'); - var g = document.querySelector('#mapSvg svg g'); + var g = document.querySelector('#mapSvg g'); if (!g) return; var svgNS = 'http://www.w3.org/2000/svg'; var line = document.createElementNS(svgNS, 'line'); @@ -1412,24 +1478,6 @@ function clearDelHighlight() { } function deleteLink(fromId, toId) { - var orphanedRooms = []; - if (mapData && mapData.links) { - var checkOrphan = function(roomId, otherId) { - var otherLinks = mapData.links.filter(function(l) { - return (l.from === roomId || l.to === roomId) && !((l.from === roomId && l.to === otherId) || (l.from === otherId && l.to === roomId)); - }); - if (otherLinks.length === 0) { - orphanedRooms.push(roomId); - } - }; - checkOrphan(toId, fromId); - checkOrphan(fromId, toId); - if (orphanedRooms.length > 0) { - var names = orphanedRooms.map(function(id) { return '#'+id; }).join(', '); - if (!confirm('Deleting this link will also delete room(s) ' + names + ' because they have no other connections. Continue?')) return; - } - } - fetch('/api/rooms/link', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, @@ -1439,20 +1487,8 @@ function deleteLink(fromId, toId) { if (!r.ok) throw new Error('unlink failed'); return r.json(); }).then(function() { - return Promise.all(orphanedRooms.map(function(id) { - return API.del('/api/rooms/' + id); - })); - }).then(function() { - var msg = 'Removed link #'+fromId+' '+toId; - if (orphanedRooms.length > 0) { - msg += ' and deleted room(s) ' + orphanedRooms.map(function(id) { return '#'+id; }).join(', '); - } - notify(msg, 'success'); + notify('Removed link #'+fromId+' '+toId, 'success'); updateUndoBar(); - if (orphanedRooms.indexOf(selectedRoom) !== -1) { - selectedRoom = null; - $('#panelContent').innerHTML = '

Room deleted

'; - } loadMap(); }).catch(function(e) { notify('Delete link failed: ' + e.message, 'error'); @@ -1471,13 +1507,44 @@ function renderDisconnectedList(data) { panel.style.display = 'flex'; dc.sort(function(a, b) { return a.id - b.id; }); list.innerHTML = dc.map(function(r) { - return '
' + + return '
' + '#' + r.id + '' + '' + esc(r.name || '') + '' + '
'; }).join(''); } +function showDcContextMenu(e, id) { + var overlay = document.createElement('div'); + overlay.className = 'cm-overlay'; + overlay.onclick = function() { overlay.remove(); menu.remove(); }; + + var menu = document.createElement('div'); + menu.className = 'cm-menu'; + menu.style.left = e.clientX + 'px'; + menu.style.top = e.clientY + 'px'; + + var delBtn = document.createElement('button'); + delBtn.textContent = 'Delete Room'; + delBtn.className = 'danger'; + delBtn.onclick = function() { + overlay.remove(); menu.remove(); + deleteRoom(id); + }; + menu.appendChild(delBtn); + + var selBtn = document.createElement('button'); + selBtn.textContent = 'Room Details'; + selBtn.onclick = function() { + overlay.remove(); menu.remove(); + selectRoom(id); + }; + menu.appendChild(selBtn); + + document.body.appendChild(overlay); + document.body.appendChild(menu); +} + function initPanelResize() { var handle = document.createElement('div'); handle.className = 'panel-resize-handle'; @@ -1499,12 +1566,14 @@ function initPanelResize() { if (newW < 300) newW = 300; if (newW > 800) newW = 800; panel.style.width = newW + 'px'; + updateView(); } function onUp() { document.body.style.userSelect = ''; document.body.style.cursor = ''; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); + updateView(); } } -- cgit v1.2.3