var CELL = 100; var currentZ = 0, currentDir = '', selectedRoom = null, mapData = null; 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 delDrag = false, delFromId = null, delTargetId = null; var roomMap = {}, occupied = {}; var gridDirs = [ {dx:0,dy:-1,dir:'north'},{dx:0,dy:1,dir:'south'}, {dx:1,dy:0,dir:'east'},{dx:-1,dy:0,dir:'west'}, {dx:1,dy:-1,dir:'northeast'},{dx:-1,dy:-1,dir:'northwest'}, {dx:1,dy:1,dir:'southeast'},{dx:-1,dy:1,dir:'southwest'} ]; var saveTimer = null; var upTarget = {}, downTarget = {}; function changeZ(dz) { if (dz === 0) currentZ = 0; else currentZ += dz; loadMap(); } function changeDir(dir) { currentDir = dir; loadMap(); } function loadMap() { $('#zLabel').textContent = 'Z=' + currentZ; var url = '/api/map?z=' + currentZ; if (currentDir) url += '&dir=' + encodeURIComponent(currentDir); API.get(url).then(function(data) { mapData = data; if (!currentDir && data.dir) { currentDir = data.dir; } updateDirSelect(); renderMap(data); }).catch(function(e) { notify('Failed to load map: ' + e.message, 'error'); }); } function renderMap(data) { var svg = $('#mapSvg'); var container = $('#mapContainer'); var W = container.clientWidth; var H = container.clientHeight; roomMap = {}; if (data.rooms) data.rooms.forEach(function(r) { roomMap[r.id] = r; }); occupied = {}; if (data.rooms) data.rooms.forEach(function(r) { occupied[r.x+','+r.y] = r.id; }); upTarget = {}; downTarget = {}; if (data.upLinks) data.upLinks.forEach(function(l) { upTarget[l.from] = l.to; }); if (data.downLinks) data.downLinks.forEach(function(l) { downTarget[l.from] = l.to; }); var svgNS = 'http://www.w3.org/2000/svg'; var g = ''; if (data.links) { data.links.forEach(function(l) { var fr = roomMap[l.from], tr = roomMap[l.to]; if (!fr || !tr) return; var x1 = fr.x * CELL + CELL/2, y1 = fr.y * CELL + CELL/2; var x2 = tr.x * CELL + CELL/2, y2 = tr.y * CELL + CELL/2; var c = fr.color ? resolveColor(fr.color) : '#666'; c = c.replace('#',''); g += ''; }); } var hasUp = {}, hasDown = {}; if (data.upLinks) data.upLinks.forEach(function(l) { hasUp[l.from] = true; }); if (data.downLinks) data.downLinks.forEach(function(l) { hasDown[l.from] = true; }); var roomHTML = ''; if (data.rooms) { data.rooms.forEach(function(r) { var x = r.x * CELL + 8, y = r.y * CELL + 8, w = CELL-16, h = CELL-16; var fill = resolveColor(r.color); var txtColor = hexLuminance(fill) > 0.35 ? '#111' : '#fff'; var sel = selectedRoom === r.id; roomHTML += ''; roomHTML += wrapText(r.name, x+w/2, y+h/2, w, txtColor); var btnY = y+h-4; var topY = y+4; if (hasUp[r.id] && upTarget[r.id]) { roomHTML += '\u2191'; } if (hasDown[r.id] && downTarget[r.id]) { roomHTML += '\u2193'; } if (sel) { var btnX = x + 6, btnBW = 18; roomHTML += '+\u2191'; roomHTML += '+\u2193'; } }); } var ghostHTML = ''; if (selectedRoom && roomMap[selectedRoom]) { var sr = roomMap[selectedRoom]; gridDirs.forEach(function(d) { var nx = sr.x + d.dx, ny = sr.y + d.dy; var key = nx+','+ny; if (!occupied[key]) { ghostHTML += ''; ghostHTML += '+'; } }); } 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.addEventListener('contextmenu', function(e) { e.preventDefault(); }); svg.onmousedown = function(e) { var onRoom = e.target.classList.contains('rm'); var onGhost = e.target.classList.contains('ghost'); var onUdBtn = e.target.classList.contains('ud-btn'); var onNavBtn = e.target.classList.contains('nav-btn'); if (onNavBtn) { e.stopPropagation(); var dz = parseInt(e.target.getAttribute('data-dz')); var tid = parseInt(e.target.getAttribute('data-target')); currentZ += dz; selectedRoom = tid; loadMap(); selectRoom(tid); return; } if (e.button === 2 && onRoom) { delFromId = parseInt(e.target.getAttribute('data-id')); delDrag = false; delTargetId = null; dragging = true; startX = e.clientX; startY = e.clientY; prevX = e.clientX; prevY = e.clientY; dragRoom = true; linkFromId = null; linkDrag = false; linkTargetId = null; linkGhostDir = null; clearLinkHighlight(); return; } if (e.button !== 0) return; if (onUdBtn) { e.stopPropagation(); var dir = e.target.getAttribute('data-dir'); createRoom(selectedRoom, dir); return; } dragRoom = onRoom || onGhost; dragging = true; startX = e.clientX; startY = e.clientY; prevX = e.clientX; prevY = e.clientY; linkFromId = onRoom ? parseInt(e.target.getAttribute('data-id')) : null; linkDrag = false; linkTargetId = null; linkGhostDir = null; delDrag = false; delFromId = null; delTargetId = null; clearLinkHighlight(); }; svg.onmousemove = function(e) { if (!dragging) return; var moved = Math.abs(e.clientX - startX) >= 4 || Math.abs(e.clientY - startY) >= 4; if (delFromId && moved && !delDrag) { delDrag = true; } if (delDrag && moved) { updateDelDragTarget(e); return; } if (!dragRoom && !delDrag && !delFromId) { panX += e.clientX - prevX; panY += e.clientY - prevY; prevX = e.clientX; prevY = e.clientY; updateView(); } if (linkFromId && moved) { linkDrag = true; updateLinkDragTarget(e); } }; svg.onmouseup = function(e) { if (!dragging) return; var moved = Math.abs(e.clientX - startX) >= 4 || Math.abs(e.clientY - startY) >= 4; if (delFromId && !delDrag && !moved) { showRoomContextMenu(startX, startY, delFromId); clearDelHighlight(); dragging = false; dragRoom = false; delFromId = null; delTargetId = null; delDrag = false; return; } if (delDrag && delFromId && delTargetId) { deleteLink(delFromId, delTargetId); clearDelHighlight(); dragging = false; dragRoom = false; delDrag = false; delFromId = null; delTargetId = null; return; } if (linkDrag && linkFromId) { if (linkTargetId) { createLink(linkFromId, linkTargetId); } else if (linkGhostDir) { createRoom(linkFromId, linkGhostDir); } } else if (!moved && !delDrag) { if (e.target.classList.contains('rm')) { selectRoom(parseInt(e.target.getAttribute('data-id'))); } else if (e.target.classList.contains('ghost')) { var dir = e.target.getAttribute('data-dir'); createRoom(selectedRoom, dir); } } clearLinkHighlight(); clearDelHighlight(); dragging = false; dragRoom = false; linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null; delDrag = false; delFromId = null; delTargetId = null; }; svg.onwheel = function(e) { e.preventDefault(); var zf = e.deltaY < 0 ? 1.1 : 0.9; scale *= zf; if (scale < 0.1) scale = 0.1; if (scale > 5) scale = 5; 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) { if (!name) name = ''; color = color || '#fff'; var charW = 6; var maxLen = Math.max(4, Math.floor((maxWidth - 4) / charW)); var lines = []; for (var i = 0; i < name.length; i += maxLen) { lines.push(name.substring(i, i + maxLen)); } if (lines.length > 3) lines = lines.slice(0, 3); var dy = -(lines.length - 1) * 6; return lines.map(function(l, i) { return ''+esc(l)+''; }).join(''); } function updateView() { var svg = document.querySelector('#mapSvg svg'); if (!svg) return; var W = svg.parentElement.clientWidth; var H = svg.parentElement.clientHeight; var vb = [(-panX - W/2)/scale, (-panY - H/2)/scale, W/scale, H/scale]; svg.setAttribute('viewBox', vb.join(' ')); } function selectRoom(id) { selectedRoom = id; loadMap(); API.get('/api/rooms/' + id).then(function(data) { renderSidePanel(data); }).catch(function(e) { notify('Failed to load room: '+e.message, 'error'); }); } function renderSidePanel(data) { var panel = $('#panelContent'); if (!data || !data.room) { panel.innerHTML = '

Room not found

'; return; } var r = data.room; var file = data.file || ''; var html = '
' + esc(file) + '
'; 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 += '
'; panel.innerHTML = html; setTimeout(function() { var cf = document.querySelector('#panelContent .color-field'); if (cf) { bindColorField(cf); var ci = cf.querySelector('input'); if (ci) ci.addEventListener('input', function() { autoSave(); }); } loadRoomDirs(function(dirs) { var sel = $('#roomDir'); if (!sel) return; var current = (file || '').replace(/^data\/rooms\/?/, '').split('/')[0] || ''; dirs.forEach(function(d) { var opt = document.createElement('option'); opt.value = d; opt.textContent = d || '(root)'; if (d === current) opt.selected = true; sel.appendChild(opt); }); }); }, 50); } function autoSave() { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(function() { doSavePanel(); }, 600); } function doSavePanel() { var id = selectedRoom; if (!id) return; API.get('/api/rooms/' + id).then(function(data) { var r = data.room || {}; r.id = id; 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; var newID = parseInt($('#roomID').value) || id; var newDir = $('#roomDir').value; var currentDir = (data.file || '').replace(/^data\/rooms\/?/, '').split('/')[0] || ''; var save = function() { API.put('/api/rooms/' + id, r).then(function() { updateUndoBar(); }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); }; if (newDir !== currentDir && newDir !== undefined) { 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() { selectRoom(newID); }).catch(function(e) { notify('Rename failed: '+e.message, 'error'); save(); }); } else { save(); } }).catch(function(e) { notify('Move failed: '+e.message, 'error'); save(); }); } 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(); }); } else { save(); } }); } function deleteRoom(id) { if (!confirm('Delete room #' + id + '?')) return; if (mapData && mapData.rooms) { var neighbors = []; if (mapData.links) { mapData.links.forEach(function(l) { if (l.from === id && l.to !== id) neighbors.push(l.to); if (l.to === id && l.from !== id) neighbors.push(l.from); }); } if (neighbors.length > 1) { var remaining = mapData.rooms.filter(function(r) { return r.id !== id; }); var adj = {}; remaining.forEach(function(r) { adj[r.id] = []; }); if (mapData.links) { mapData.links.forEach(function(l) { if (l.from === id || l.to === id) return; if (adj[l.from]) adj[l.from].push(l.to); if (adj[l.to]) adj[l.to].push(l.from); }); } if (remaining.length > 0) { var visited = {}, queue = [remaining[0].id]; visited[remaining[0].id] = true; while (queue.length > 0) { var cur = queue.shift(); (adj[cur] || []).forEach(function(n) { if (!visited[n]) { visited[n] = true; queue.push(n); } }); } if (Object.keys(visited).length < remaining.length) { if (!confirm('Deleting room #'+id+' will split the map into disconnected areas. Continue?')) return; } } } } API.del('/api/rooms/' + id).then(function() { notify('Room #' + id + ' deleted', 'success'); updateUndoBar(); selectedRoom = null; $('#panelContent').innerHTML = '

Room deleted

'; loadMap(); }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); } function createRoom(fromID, dir) { API.get('/api/next-room-id?from=' + fromID).then(function(r) { var newID = r.id; var name = 'Room #' + newID; var body = { name: name, link_from: fromID, link_dir: dir }; var srcRoom = roomMap[fromID]; if (srcRoom && srcRoom.color) body.color = srcRoom.color; API.post('/api/rooms', body).then(function(resp) { var createdId = (resp && resp.room && resp.room.id) || newID; notify('Room #' + createdId + ' created', 'success'); updateUndoBar(); selectRoom(createdId); }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); }); }); } function showRoomContextMenu(x, y, 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 = x + 'px'; menu.style.top = y + '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 mouseToGrid(e) { var svgEl = document.querySelector('#mapSvg svg'); if (!svgEl) return null; var rect = svgEl.getBoundingClientRect(); var mx = e.clientX - rect.left; var my = e.clientY - rect.top; if (mx < 0 || my < 0 || mx > rect.width || my > rect.height) return null; var vb = svgEl.getAttribute('viewBox'); if (!vb) return null; var parts = vb.split(' '); var vbX = parseFloat(parts[0]), vbY = parseFloat(parts[1]); var vbW = parseFloat(parts[2]), vbH = parseFloat(parts[3]); var wx = vbX + mx * vbW / rect.width; var wy = vbY + my * vbH / rect.height; return { x: Math.floor(wx / CELL), y: Math.floor(wy / CELL) }; } function gridDeltaToDir(dx, dy) { for (var i = 0; i < gridDirs.length; i++) { if (gridDirs[i].dx === dx && gridDirs[i].dy === dy) return gridDirs[i].dir; } return null; } function updateLinkDragTarget(e) { clearLinkHighlight(); linkTargetId = null; linkGhostDir = null; var fromRoom = mapData.rooms ? mapData.rooms.find(function(r){return r.id === linkFromId;}) : null; if (!fromRoom) return; var gp = mouseToGrid(e); if (!gp) return; var neighbor = occupied[gp.x+','+gp.y]; var gdx = gp.x - fromRoom.x; var gdy = gp.y - fromRoom.y; if (Math.abs(gdx) > 1 || Math.abs(gdy) > 1) return; if (gdx === 0 && gdy === 0) return; var dir = gridDeltaToDir(gdx, gdy); if (!dir) return; if (neighbor && neighbor !== linkFromId) { linkTargetId = neighbor; linkGhostDir = null; applyLinkHighlight(linkFromId, linkTargetId); } else if (!neighbor) { linkGhostDir = dir; linkTargetId = null; applyGhostLinkHighlight(fromRoom, gp.x, gp.y); } else { linkGhostDir = null; } } function applyGhostLinkHighlight(fromRoom, gx, gy) { clearLinkHighlight(); linkTargetId = null; var fromEl = document.querySelector('.rm[data-id="'+linkFromId+'"]'); if (!fromEl) return; fromEl.setAttribute('stroke', '#0ff'); fromEl.setAttribute('stroke-width', '3'); var g = document.querySelector('#mapSvg svg g'); if (!g) return; var svgNS = 'http://www.w3.org/2000/svg'; var line = document.createElementNS(svgNS, 'line'); line.id = '__hl_line'; line.setAttribute('x1', fromRoom.x * CELL + CELL/2); line.setAttribute('y1', fromRoom.y * CELL + CELL/2); line.setAttribute('x2', gx * CELL + CELL/2); line.setAttribute('y2', gy * CELL + CELL/2); line.setAttribute('stroke', '#0ff'); line.setAttribute('stroke-width', '3'); line.setAttribute('stroke-dasharray', '6,3'); line.setAttribute('pointer-events', 'none'); g.appendChild(line); } function applyLinkHighlight(fromId, toId) { clearLinkHighlight(); var fromEl = document.querySelector('.rm[data-id="'+fromId+'"]'); var toEl = document.querySelector('.rm[data-id="'+toId+'"]'); var fromRoom = roomMap[fromId]; var toRoom = roomMap[toId]; if (!fromEl || !toEl || !fromRoom || !toRoom) return; fromEl.setAttribute('stroke', '#0ff'); fromEl.setAttribute('stroke-width', '3'); toEl.setAttribute('stroke', '#f0f'); toEl.setAttribute('stroke-width', '3'); var g = document.querySelector('#mapSvg svg g'); if (!g) return; var svgNS = 'http://www.w3.org/2000/svg'; var line = document.createElementNS(svgNS, 'line'); line.id = '__hl_line'; line.setAttribute('x1', fromRoom.x * CELL + CELL/2); line.setAttribute('y1', fromRoom.y * CELL + CELL/2); line.setAttribute('x2', toRoom.x * CELL + CELL/2); line.setAttribute('y2', toRoom.y * CELL + CELL/2); line.setAttribute('stroke', '#f0f'); line.setAttribute('stroke-width', '3'); line.setAttribute('stroke-dasharray', '6,3'); line.setAttribute('pointer-events', 'none'); g.appendChild(line); } function clearLinkHighlight() { var line = document.getElementById('__hl_line'); if (line) line.remove(); if (linkFromId) { var el = document.querySelector('.rm[data-id="'+linkFromId+'"]'); if (el) { el.setAttribute('stroke', selectedRoom === linkFromId ? '#fff' : (el.getAttribute('fill') || '#3a3a6a')); el.setAttribute('stroke-width', selectedRoom === linkFromId ? '3' : '1.5'); } } if (linkTargetId) { var el = document.querySelector('.rm[data-id="'+linkTargetId+'"]'); if (el) { el.setAttribute('stroke', selectedRoom === linkTargetId ? '#fff' : (el.getAttribute('fill') || '#3a3a6a')); el.setAttribute('stroke-width', selectedRoom === linkTargetId ? '3' : '1.5'); } } } function createLink(fromId, toId) { API.post('/api/rooms/link', {from: fromId, to: toId}).then(function() { notify('Linked rooms #'+fromId+' '+toId, 'success'); updateUndoBar(); loadMap(); if (selectedRoom) { API.get('/api/rooms/' + selectedRoom).then(function(data) { renderSidePanel(data); }); } }).catch(function(e) { notify('Link failed: ' + e.message, 'error'); }); } function updateDelDragTarget(e) { clearDelHighlight(); delTargetId = null; var fromRoom = mapData.rooms ? mapData.rooms.find(function(r){return r.id === delFromId;}) : null; if (!fromRoom) return; var gp = mouseToGrid(e); if (!gp) return; var neighbor = occupied[gp.x+','+gp.y]; if (!neighbor || neighbor === delFromId) return; var gdx = gp.x - fromRoom.x; var gdy = gp.y - fromRoom.y; if (Math.abs(gdx) > 1 || Math.abs(gdy) > 1) return; if (gdx === 0 && gdy === 0) return; if (!gridDeltaToDir(gdx, gdy)) return; delTargetId = neighbor; applyDelHighlight(delFromId, delTargetId); } function applyDelHighlight(fromId, toId) { clearDelHighlight(); var fromEl = document.querySelector('.rm[data-id="'+fromId+'"]'); var toEl = document.querySelector('.rm[data-id="'+toId+'"]'); var fromRoom = roomMap[fromId]; var toRoom = roomMap[toId]; if (!fromEl || !toEl || !fromRoom || !toRoom) return; fromEl.setAttribute('stroke', '#f55'); fromEl.setAttribute('stroke-width', '3'); toEl.setAttribute('stroke', '#f55'); toEl.setAttribute('stroke-width', '3'); var g = document.querySelector('#mapSvg svg g'); if (!g) return; var svgNS = 'http://www.w3.org/2000/svg'; var line = document.createElementNS(svgNS, 'line'); line.id = '__dl_line'; line.setAttribute('x1', fromRoom.x * CELL + CELL/2); line.setAttribute('y1', fromRoom.y * CELL + CELL/2); line.setAttribute('x2', toRoom.x * CELL + CELL/2); line.setAttribute('y2', toRoom.y * CELL + CELL/2); line.setAttribute('stroke', '#f55'); line.setAttribute('stroke-width', '3'); line.setAttribute('stroke-dasharray', '6,3'); line.setAttribute('pointer-events', 'none'); g.appendChild(line); } function clearDelHighlight() { var line = document.getElementById('__dl_line'); if (line) line.remove(); if (delFromId) { var el = document.querySelector('.rm[data-id="'+delFromId+'"]'); if (el) { el.setAttribute('stroke', selectedRoom === delFromId ? '#fff' : (el.getAttribute('fill') || '#3a3a6a')); el.setAttribute('stroke-width', selectedRoom === delFromId ? '3' : '1.5'); } } if (delTargetId) { var el = document.querySelector('.rm[data-id="'+delTargetId+'"]'); if (el) { el.setAttribute('stroke', selectedRoom === delTargetId ? '#fff' : (el.getAttribute('fill') || '#3a3a6a')); el.setAttribute('stroke-width', selectedRoom === delTargetId ? '3' : '1.5'); } } } function deleteLink(fromId, toId) { if (mapData && mapData.links) { var checkOrphan = function(roomId, otherId) { var room = roomMap[roomId]; if (!room) return false; 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) { if (!confirm('Removing this link will orphan room #'+roomId+' and it will disappear from the map. Continue?')) return false; } return true; }; if (!checkOrphan(toId, fromId)) return; if (!checkOrphan(fromId, toId)) return; } fetch('/api/rooms/link', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({from: fromId, to: toId}), credentials: 'same-origin' }).then(function(r) { if (!r.ok) throw new Error('unlink failed'); return r.json(); }).then(function() { notify('Removed link #'+fromId+' '+toId, 'success'); updateUndoBar(); loadMap(); if (selectedRoom) { API.get('/api/rooms/' + selectedRoom).then(function(data) { renderSidePanel(data); }); } }).catch(function(e) { notify('Delete link failed: ' + e.message, 'error'); }); } function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function escAttr(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(/"/g,'"'); } window.refreshPage = function() { loadMap(); if (selectedRoom) selectRoom(selectedRoom); }; function loadRoomDirs(cb) { API.get('/api/room-dirs').then(function(dirs) { var sel = $('#dirSelect'); if (sel && sel.options.length === 0) { dirs.forEach(function(d) { var opt = document.createElement('option'); opt.value = d; opt.textContent = d || '(root)'; sel.appendChild(opt); }); } updateDirSelect(); if (cb) cb(dirs); }); } function updateDirSelect() { var sel = $('#dirSelect'); if (sel && currentDir !== undefined) { sel.value = currentDir; } } document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); });