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 dirChangedByUser = false; var currentPanelTab = 0; 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; dirChangedByUser = true; selectedRoom = null; loadMap(); } function zoomIn() { scale *= 1.1; if (scale > 5) scale = 5; updateView(); } function zoomOut() { scale *= 0.9; if (scale < 0.1) scale = 0.1; updateView(); } function loadMap() { $('#zLabel').textContent = 'Z=' + currentZ; var url = '/api/map?z=' + currentZ; if (currentDir) url += '&dir=' + encodeURIComponent(currentDir); var wasDirChange = dirChangedByUser; dirChangedByUser = false; return API.get(url).then(function(data) { mapData = data; if (!currentDir && data.dir) { currentDir = data.dir; } updateDirSelect(); renderMap(data); renderDisconnectedList(data); if (wasDirChange && data.seed) { selectRoom(data.seed); } }).catch(function(e) { notify('Failed to load map: ' + e.message, 'error'); }); } function blendColors(colorA, colorB) { if (!colorA && !colorB) return '#666666'; if (!colorB) return xtermToCss(parseXtermIdx(colorA)); if (!colorA) return xtermToCss(parseXtermIdx(colorB)); var idxA = parseXtermIdx(colorA); var idxB = parseXtermIdx(colorB); var rgbA = xtermToRGB(idxA); var rgbB = xtermToRGB(idxB); var r = Math.round((rgbA[0] + rgbB[0]) / 2); var g = Math.round((rgbA[1] + rgbB[1]) / 2); var b = Math.round((rgbA[2] + rgbB[2]) / 2); return '#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1).toUpperCase(); } function parseXtermIdx(v) { if (!v) return 0; v = String(v).toUpperCase().replace(/[^0-9A-F]/g, ''); if (v.length >= 2) return parseInt(v.substring(0, 2), 16) || 0; return 0; } function renderMap(data) { var svg = $('#mapSvg'); var container = $('#mapContainer'); var W = container.clientWidth; var H = container.clientHeight; roomMap = {}; occupied = {}; upTarget = {}; downTarget = {}; var hasUp = {}, hasDown = {}; if (data.rooms) data.rooms.forEach(function(r) { roomMap[r.id] = r; occupied[r.x+','+r.y] = r.id; }); if (data.upLinks) data.upLinks.forEach(function(l) { upTarget[l.from] = l.to; hasUp[l.from] = true; }); if (data.downLinks) data.downLinks.forEach(function(l) { downTarget[l.from] = l.to; hasDown[l.from] = true; }); 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 = blendColors(fr.color, tr.color).replace('#',''); g += ''; }); } 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 += ''; 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+2, w, txtColor); if (hasUp[r.id] && upTarget[r.id]) { roomHTML += '\u2191'; } if (hasDown[r.id] && downTarget[r.id]) { roomHTML += '\u2193'; } if (sel) { var 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]; svg.setAttribute('viewBox', vb.join(' ')); svg.setAttribute('width', W); svg.setAttribute('height', H); svg.innerHTML = '' + innerHTML + ''; svg.oncontextmenu = 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; loadMap().then(function() { 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(); }; } 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 words = name.split(/\s+/); var lines = []; 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) { return ''+esc(l)+''; }).join(''); } function updateView() { var svg = $('#mapSvg'); 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) { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } var prev = selectedRoom; selectedRoom = id; if (mapData && roomMap) { if (prev && prev !== id) { var prevEl = document.querySelector('.rm[data-id="'+prev+'"]'); if (prevEl) { prevEl.setAttribute('stroke', prevEl.getAttribute('fill') || '#3a3a6a'); prevEl.setAttribute('stroke-width', '1.5'); } } var el = document.querySelector('.rm[data-id="'+id+'"]'); if (el) { el.setAttribute('stroke', '#fff'); el.setAttribute('stroke-width', '3'); } } API.get('/api/rooms/' + id).then(function(data) { renderSidePanel(data); }).catch(function(e) { notify('Failed to load room: '+e.message, 'error'); }); } var currentRoomData = null; function renderSidePanel(data) { var panel = $('#panelContent'); if (!data || !data.room) { panel.innerHTML = '

Room not found

'; return; } currentRoomData = data; var r = data.room; var file = data.file || ''; var objects = Array.isArray(r.objects) ? r.objects : []; var localObjs = objects.filter(function(o) { return !o.id; }); var refObjs = objects.filter(function(o) { return !!o.id; }); var spawns = Array.isArray(r.item_spawns) ? r.item_spawns : []; var mobs = Array.isArray(r.mobs) ? r.mobs : []; var html = '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
'; html += '
'; if (currentPanelTab === 0) html += renderRoomTab(r, file); 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; 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 = getRoomDirFromFile(file); sel.innerHTML = ''; 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 renderRoomTab(r, file) { var html = '
' + esc(file) + '
'; 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 += '
'; 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('/'); if (idx >= 0) { var subIdx = rel.indexOf('/', idx + 1); if (subIdx >= 0) return rel.substring(0, subIdx); return rel.substring(0, idx); } return ''; } function switchPanelTab(idx) { currentPanelTab = idx; if (!currentRoomData) return; var r = currentRoomData.room; var file = currentRoomData.file || ''; var objects = Array.isArray(r.objects) ? r.objects : []; var localObjs = objects.filter(function(o) { return !o.id; }); var refObjs = objects.filter(function(o) { return !!o.id; }); var spawns = Array.isArray(r.item_spawns) ? r.item_spawns : []; var mobs = Array.isArray(r.mobs) ? r.mobs : []; var html = ''; if (idx === 0) html = renderRoomTab(r, file); 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'); tabs.forEach(function(t, i) { t.classList.toggle('active', i === idx); }); if (idx === 0) { setTimeout(function() { var cf = document.querySelector('#panelContent .color-field'); if (cf) bindColorField(cf); loadRoomDirs(function(dirs) { var sel = $('#roomDir'); if (!sel) return; sel.innerHTML = ''; var current = getRoomDirFromFile(file); 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 renderLocalTab(localObjs) { var h = ''; localObjs.forEach(function(obj, i) { var descText = ''; if (Array.isArray(obj.description)) { descText = obj.description.map(function(d) { return d.text || ''; }).join('\n'); } else if (typeof obj.description === 'string') { descText = obj.description; } h += '
'; h += '
'; h += ''; h += ''; h += '
'; h += ''; h += ''; h += '
'; h += '
'; h += ''; h += '
'; }); h += ''; return h; } function renderObjectsTab(refObjs) { var h = ''; refObjs.forEach(function(obj, i) { h += '
'; h += '
' + esc(obj.id || '') + '
'; h += ''; h += ''; h += '
'; }); h += ''; return h; } function renderMobsTab(mobs) { var h = ''; mobs.forEach(function(mob, i) { h += '
'; h += '
' + esc(mob.id || '') + '
'; h += ''; h += ''; h += '
'; }); h += ''; return h; } function renderItemsTab(spawns) { var h = ''; spawns.forEach(function(spawn, i) { h += '
'; h += '
'; h += '' + esc(spawn.id || '') + ''; h += '
'; h += '
'; h += '
'; h += '
'; h += '
'; h += ''; h += ''; h += '
'; }); h += ''; return h; } function renderExitsSection(r) { var exits = r.exits || {}; var exitDirs = ['north','south','east','west','northeast','northwest','southeast','southwest','up','down']; var h = '

Exits

'; exitDirs.forEach(function(d) { var exit = exits[d]; if (!exit) return; var target = typeof exit === 'object' ? exit.room : exit; var cond = (typeof exit === 'object' && exit.condition) ? exit.condition : null; 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 += '# ' + target + ''; if (bmsg) h += 'Blocked: ' + esc(bmsg) + ''; if (cond) h += 'Conditional'; h += ''; h += ''; h += '
'; }); h += '
'; h += ''; h += ''; h += ''; h += '
'; h += '
'; return h; } var editingExitDir = null; function editExitCondition(dir) { editingExitDir = dir; var exits = (currentRoomData && currentRoomData.room && currentRoomData.room.exits) || {}; var exit = exits[dir]; if (!exit) return; if (typeof exit !== 'object') { exit = {room: exit}; } var cond = exit.condition || null; var bmsg = exit.blocked_message || ''; var setFlags = exit.set_flags || null; var setPlayerFlags = exit.set_player_flags || null; var flagsStr = ''; if (setFlags) flagsStr = Object.keys(setFlags).map(function(k) { return k + '=' + setFlags[k]; }).join(', '); var pflagsStr = ''; if (setPlayerFlags) pflagsStr = Object.keys(setPlayerFlags).map(function(k) { return k + '=' + setPlayerFlags[k]; }).join(', '); var parsed = parseCondition(cond); var overlay = document.createElement('div'); 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:520px;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 += '
'; h += ''; h += '
'; h += ''; h += ''; h += ''; h += '
'; menu.innerHTML = h; document.body.appendChild(overlay); document.body.appendChild(menu); overlay.onclick = closeExitModal; menu._condClauses = parsed.clauses; menu._condMode = parsed.mode; renderConditionClauses(menu); } function parseCondition(cond) { var clauses = []; if (!cond || Object.keys(cond).length === 0) return {clauses: clauses, mode: 'all'}; var arr = cond.all_of; if (!arr || !Array.isArray(arr) || arr.length === 0) arr = cond.any_of; if (arr && Array.isArray(arr) && arr.length > 0) { arr.forEach(function(c) { var clause = extractClause(c); if (clause) clauses.push(clause); }); return {clauses: clauses, mode: 'all'}; } var clause = extractClause(cond); if (clause) clauses.push(clause); return {clauses: clauses, mode: 'all'}; } function extractClause(c) { var not = c.not === true; if (c.player_flag && c.player_flag !== '') return {type: 'player_flag', value: c.player_flag, not: not}; if (c.flag && c.flag !== '') return {type: 'flag', value: c.flag, not: not}; if (c.has_item && c.has_item !== '') return {type: 'has_item', value: c.has_item, not: not}; if (c.min_credits && c.min_credits !== 0) return {type: 'min_credits', value: String(c.min_credits), not: not}; if (c.value && typeof c.value === 'object' && c.value.key) return {type: 'value', value: c.value.key + '=' + (c.value.value || ''), not: not}; return null; } function renderConditionClauses(menu) { var container = menu.querySelector('#condClauses'); if (!container) return; var clauses = menu._condClauses || []; var h = ''; clauses.forEach(function(clause, i) { h += '
'; h += ''; if (clause.type === 'value') { var kv = clause.value ? clause.value.split('=', 2) : ['','']; var kn = (kv[0] || '').trim(); var kvv = (kv[1] || '').trim(); h += ''; h += '='; h += ''; } else { h += ''; } h += ''; h += ''; h += '
'; }); container.innerHTML = h; } function updateClauseData(el) { var clauseEl = el.closest('.cond-clause'); var menu = el.closest('.exit-modal'); if (!clauseEl || !menu) return; var idx = parseInt(clauseEl.getAttribute('data-idx')); var clause = menu._condClauses[idx]; if (!clause) return; clause.type = clauseEl.querySelector('.cond-type').value; clause.not = clauseEl.querySelector('.cond-not').checked; if (clause.type === 'value') { var k = clauseEl.querySelector('.cond-val-key'); var v = clauseEl.querySelector('.cond-val-val'); clause.value = (k ? k.value.trim() : '') + '=' + (v ? v.value.trim() : ''); } else { var val = clauseEl.querySelector('.cond-val'); clause.value = val ? val.value : ''; } } function addConditionClause() { var menu = document.querySelector('.exit-modal'); if (!menu) return; if (!menu._condClauses) menu._condClauses = []; menu._condClauses.push({type: 'player_flag', value: '', not: false}); renderConditionClauses(menu); } function removeConditionClause(btn) { var menu = btn.closest('.exit-modal'); var clauseEl = btn.closest('.cond-clause'); if (!menu || !clauseEl) return; var idx = parseInt(clauseEl.getAttribute('data-idx')); menu._condClauses.splice(idx, 1); renderConditionClauses(menu); } function closeExitModal() { editingExitDir = null; var ov = document.querySelector('.exit-overlay'); if (ov) ov.remove(); var m = document.querySelector('.exit-modal'); if (m) m.remove(); } function saveExitCondition() { if (!editingExitDir || !currentRoomData || !currentRoomData.room) return; var menu = document.querySelector('.exit-modal'); if (!menu) return; var r = currentRoomData.room; var exits = r.exits || {}; var exit = exits[editingExitDir]; if (!exit) return; if (typeof exit !== 'object') { exit = {room: exit}; exits[editingExitDir] = exit; r.exits = exits; } var bmsg = document.getElementById('exitBmsg'); exit.blocked_message = (bmsg && bmsg.value) ? bmsg.value : ''; var flagsStr = document.getElementById('exitFlags'); var pflagsStr = document.getElementById('exitPFlags'); exit.set_flags = parseFlagInput(flagsStr ? flagsStr.value : ''); exit.set_player_flags = parseFlagInput(pflagsStr ? pflagsStr.value : ''); var clauseEls = menu.querySelectorAll('.cond-clause'); clauseEls.forEach(function(el) { updateClauseData(el); }); var clauses = (menu._condClauses || []).filter(function(c) { return c.value && c.value !== '' && c.value !== '='; }); var condition = null; if (clauses.length === 1) { condition = buildSingleCondition(clauses[0]); } else if (clauses.length > 1) { condition = {all_of: clauses.map(function(c) { return buildSingleCondition(c); })}; } exit.condition = condition; r.exits = exits; autoSave(); closeExitModal(); reRenderPanel(); } function buildSingleCondition(clause) { var obj = {}; if (clause.not) obj.not = true; if (clause.type === 'value') { var parts = (clause.value || '').split('=', 2); obj.value = {key: (parts[0] || '').trim(), value: (parts[1] || '').trim()}; } else { obj[clause.type] = clause.value; } return obj; } function clearExitCondition() { var menu = document.querySelector('.exit-modal'); if (!menu) return; menu._condClauses = []; renderConditionClauses(menu); var bmsg = document.getElementById('exitBmsg'); if (bmsg) bmsg.value = ''; var ef = document.getElementById('exitFlags'); if (ef) ef.value = ''; var pf = document.getElementById('exitPFlags'); if (pf) pf.value = ''; } function parseFlagInput(val) { if (!val || !val.trim()) return null; var m = {}; val.split(',').forEach(function(pair) { var kv = pair.trim().split('='); if (kv.length === 2) { var v = kv[1].trim(); if (v === 'true') v = true; else if (v === 'false') v = false; else if (!isNaN(v)) v = parseFloat(v); m[kv[0].trim()] = v; } }); return Object.keys(m).length > 0 ? m : null; } function addExit() { if (!selectedRoom || !currentRoomData) return; 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'); updateUndoBar(); loadMap().then(function() { selectRoom(selectedRoom); }); }).catch(function(e) { notify('Add exit failed: ' + extractError(e), 'error'); }); } 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(); }).then(function() { notify('Removed exit ' + dir + ' to #' + target, 'success'); updateUndoBar(); loadMap().then(function() { selectRoom(selectedRoom); }); }).catch(function(e) { notify('Delete exit failed: ' + extractError(e), 'error'); }); } function extractError(e) { var msg = e.message || 'unknown error'; try { var j = JSON.parse(msg); if (j.error) msg = j.error; } catch(_) {} return msg; } function getRoomObjects() { if (!currentRoomData || !currentRoomData.room) return []; var objects = currentRoomData.room.objects || []; if (!Array.isArray(objects)) return []; return objects; } function setRoomObjects(objs) { if (!currentRoomData || !currentRoomData.room) return; currentRoomData.room.objects = objs; saveNow(); } function getRoomSpawns() { if (!currentRoomData || !currentRoomData.room) return []; var spawns = currentRoomData.room.item_spawns || []; if (!Array.isArray(spawns)) return []; return spawns; } function setRoomSpawns(spawns) { if (!currentRoomData || !currentRoomData.room) return; currentRoomData.room.item_spawns = spawns; saveNow(); } function getRoomMobs() { if (!currentRoomData || !currentRoomData.room) return []; var mobs = currentRoomData.room.mobs || []; if (!Array.isArray(mobs)) return []; return mobs; } function setRoomMobs(mobs) { if (!currentRoomData || !currentRoomData.room) return; currentRoomData.room.mobs = mobs; saveNow(); } function addLocalObject() { var objs = getRoomObjects(); objs.push({name: '', description: [{text: ''}], aliases: [], hidden: false}); setRoomObjects(objs); reRenderPanel(); } function updateLocalObject(i) { var objs = getRoomObjects(); var localObjs = objs.filter(function(o) { return !o.id; }); if (i < 0 || i >= localObjs.length) return; var row = document.querySelector('.panel-tab-content .entry-row[data-idx="' + i + '"]'); if (!row) return; var nameEl = row.querySelector('.local-name'); var descEl = row.querySelector('.local-desc'); var aliasesEl = row.querySelector('.local-aliases'); var hiddenEl = row.querySelector('.local-hidden'); localObjs[i].name = nameEl ? nameEl.value : ''; var descVal = descEl ? descEl.value : ''; if (descVal) { localObjs[i].description = descVal.split('\n').map(function(line) { return {text: line}; }); } else { localObjs[i].description = []; } if (aliasesEl && aliasesEl.value) { localObjs[i].aliases = aliasesEl.value.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; }); } else { localObjs[i].aliases = []; } localObjs[i].hidden = hiddenEl ? hiddenEl.checked : false; var refObjs = objs.filter(function(o) { return !!o.id; }); setRoomObjects(localObjs.concat(refObjs)); } function removeLocalObject(i) { var objs = getRoomObjects(); var localObjs = objs.filter(function(o) { return !o.id; }); if (i < 0 || i >= localObjs.length) return; localObjs.splice(i, 1); var refObjs = objs.filter(function(o) { return !!o.id; }); setRoomObjects(localObjs.concat(refObjs)); reRenderPanel(); } function removeRefObject(i) { var objs = getRoomObjects(); var refObjs = objs.filter(function(o) { return !!o.id; }); if (i < 0 || i >= refObjs.length) return; refObjs.splice(i, 1); var localObjs = objs.filter(function(o) { return !o.id; }); setRoomObjects(localObjs.concat(refObjs)); reRenderPanel(); } function addRefObject(id) { var objs = getRoomObjects(); objs.push({id: id}); setRoomObjects(objs); reRenderPanel(); } function searchRefObjects(input) { searchAndShow(input, 'objects', function(r) { addRefObject(r.id); }); } function removeSpawn(i) { var spawns = getRoomSpawns(); spawns.splice(i, 1); setRoomSpawns(spawns); reRenderPanel(); } function updateSpawn(i) { var spawns = getRoomSpawns(); if (i < 0 || i >= spawns.length) return; var row = document.querySelector('.panel-tab-content .entry-row[data-idx="' + i + '"]'); if (!row) return; var qtyEl = row.querySelector('.spawn-qty'); var respawnEl = row.querySelector('.spawn-respawn'); if (qtyEl) spawns[i].quantity = parseInt(qtyEl.value) || 0; if (respawnEl) spawns[i].respawn_ticks = parseInt(respawnEl.value) || 0; setRoomSpawns(spawns); } function addSpawn(id) { var spawns = getRoomSpawns(); spawns.push({id: id, quantity: 1, respawn_ticks: 0}); setRoomSpawns(spawns); reRenderPanel(); } function searchItemSpawns(input) { searchAndShow(input, 'items', function(r) { addSpawn(r.id); }); } function removeMob(i) { var mobs = getRoomMobs(); mobs.splice(i, 1); setRoomMobs(mobs); reRenderPanel(); } function addMob(id) { var mobs = getRoomMobs(); mobs.push({id: id}); setRoomMobs(mobs); reRenderPanel(); } function searchMobs(input) { searchAndShow(input, 'mobs', function(r) { addMob(r.id); }); } function searchAndShow(input, type, onSelect) { var val = input.value.trim(); var results = input.parentElement.querySelector('.search-results'); if (!val) { results.style.display = 'none'; results.innerHTML = ''; return; } API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) { var items = []; 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
'; return; } results.style.display = 'block'; results.innerHTML = items.map(function(r) { return '
' + esc(r.id) + ' ' + esc(r.name || '') + '
'; }).join(''); results._items = items; results._onSelect = onSelect; }).catch(function() { results.style.display = 'none'; }); } function selectSearchResult(el) { var results = el.parentElement; var idx = Array.prototype.indexOf.call(results.children, el); if (results._items && results._items[idx] && results._onSelect) { results._onSelect(results._items[idx]); results.style.display = 'none'; results.innerHTML = ''; var input = results.parentElement.querySelector('.search-input'); if (input) input.value = ''; } } function openEditor(type, id) { window.open('/editor/' + type + '#' + id, '_blank'); } function reRenderPanel() { if (!currentRoomData) return; renderSidePanel(currentRoomData); } function autoSave() { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(function() { doSavePanel(); }, 600); } function saveNow() { if (saveTimer) clearTimeout(saveTimer); saveTimer = null; doSavePanel(); } async function doSavePanel() { var id = selectedRoom; if (!id) return; try { var data = await API.get('/api/rooms/' + id); 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 = $('#roomBlockTransport'); if (el) r.block_transport = el.checked; if (currentRoomData && currentRoomData.room) { r.objects = currentRoomData.room.objects; 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; } 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 (dirEl && newDir !== currentDir) { await API.post('/api/rooms/move', {id: id, dir: newDir}); } if (newID !== id) { await API.post('/api/rooms/rename', {id: id, new_id: newID}); selectedRoom = newID; id = newID; } await API.put('/api/rooms/' + id, r); notify('Room #' + id + ' saved', 'success'); updateUndoBar(); await loadMap(); selectRoom(id); } catch (e) { notify('Save failed: ' + extractError(e), 'error'); } } function deleteRoom(id) { var cb = document.getElementById('confirmDelete'); if (!cb || cb.checked) { 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 (mapData.upLinks) { mapData.upLinks.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 (mapData.downLinks) { mapData.downLinks.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 (mapData.upLinks) { mapData.upLinks.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 (mapData.downLinks) { mapData.downLinks.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(); loadMap().then(function() { 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 = $('#mapSvg'); 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 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 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().then(function() { if (selectedRoom) selectRoom(selectedRoom); }); }).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 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) { 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(); }).catch(function(e) { notify('Delete link failed: ' + e.message, 'error'); }); } function renderDisconnectedList(data) { var panel = $('#disconnectedPanel'); var list = $('#disconnectedList'); if (!panel || !list) return; var dc = data.disconnected || []; if (dc.length === 0) { panel.style.display = 'none'; return; } panel.style.display = 'flex'; dc.sort(function(a, b) { return a.id - b.id; }); list.innerHTML = dc.map(function(r) { 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'; var panel = $('#sidePanel'); if (!panel) return; panel.appendChild(handle); panel.style.width = Math.min(800, window.innerWidth / 3) + 'px'; var startX, startW; handle.addEventListener('mousedown', function(e) { e.preventDefault(); startX = e.clientX; startW = panel.offsetWidth; document.body.style.userSelect = 'none'; document.body.style.cursor = 'col-resize'; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); function onMove(e) { var newW = startW + (startX - e.clientX); 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(); } } 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().then(function() { 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(); }); initPanelResize(); });