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 = '';
- 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 = '
';
+ html += '';
+ html += '';
+ 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 = '