diff options
Diffstat (limited to 'internal/admin/static/map.js')
| -rw-r--r-- | internal/admin/static/map.js | 853 |
1 files changed, 812 insertions, 41 deletions
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index 3443339..ca454a0 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -6,6 +6,8 @@ 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'}, @@ -22,13 +24,29 @@ function changeZ(dz) { 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; API.get(url).then(function(data) { mapData = data; if (!currentDir && data.dir) { @@ -36,11 +54,36 @@ function loadMap() { } 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) { + var idxA = parseXtermIdx(colorA); + var idxB = parseXtermIdx(colorB); + if (colorA && !colorB) return xtermToCss(idxA); + if (!colorA && colorB) return xtermToCss(idxB); + if (!colorA && !colorB) return '#666666'; + 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'); @@ -66,9 +109,9 @@ function renderMap(data) { 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'; + var c = blendColors(fr.color, tr.color); c = c.replace('#',''); - g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#'+c+'" stroke-width="6" opacity="0.35"'+ (l.bidirectional ? '' : ' stroke-dasharray="4,3"')+' vector-effect="non-scaling-stroke"/>'; + g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#'+c+'" stroke-width="6" opacity="0.55"'+ (l.bidirectional ? '' : ' stroke-dasharray="4,3"')+' vector-effect="non-scaling-stroke"/>'; }); } @@ -84,6 +127,8 @@ function renderMap(data) { var txtColor = hexLuminance(fill) > 0.35 ? '#111' : '#fff'; var sel = selectedRoom === r.id; roomHTML += '<rect class="rm" x="'+x+'" y="'+y+'" width="'+w+'" height="'+h+'" rx="5" fill="'+fill+'" stroke="'+(sel?'#fff':fill)+'" stroke-width="'+(sel?3:1.5)+'" data-id="'+r.id+'" data-name="'+esc(r.name)+'"/>'; + var dimColor = hexLuminance(fill) > 0.35 ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.4)'; + roomHTML += '<text x="'+(x+w/2)+'" y="'+(y+h/2-10)+'" text-anchor="middle" font-size="8" fill="'+dimColor+'" font-family="monospace" pointer-events="none">#'+r.id+'</text>'; roomHTML += wrapText(r.name, x+w/2, y+h/2, w, txtColor); var btnY = y+h-4; @@ -296,29 +341,39 @@ function selectRoom(id) { }); } +var currentRoomData = null; + function renderSidePanel(data) { var panel = $('#panelContent'); if (!data || !data.room) { panel.innerHTML = '<p style="color:#888;text-align:center;margin-top:40px">Room not found</p>'; return; } + currentRoomData = data; var r = data.room; var file = data.file || ''; - var html = '<div class="form-group"><label>File</label><span style="font-size:11px;color:#888">' + esc(file) + '</span></div>'; - html += '<div class="form-group"><label>ID</label><input id="roomID" type="number" value="' + r.id + '" onchange="autoSave()"></div>'; - html += '<div class="form-group"><label>Name</label><input id="roomName" value="' + escAttr(r.name || '') + '" oninput="autoSave()"></div>'; - html += '<div class="form-group color-field"><label>Color</label><span class="color-swatch" style="background:'+resolveColor(r.color)+'"></span><input id="roomColor" value="' + escAttr(r.color || '') + '" oninput="autoSave()"></div>'; - html += '<div class="form-group"><label>Directory</label><select id="roomDir" onchange="autoSave()"></select></div>'; - 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 += '<div class="form-group"><label>Description</label><textarea id="roomDesc" rows="4" oninput="autoSave()">' + esc(descText) + '</textarea></div>'; - html += '<div class="form-group"><label>Hazard</label><input id="roomHazard" value="' + escAttr(r.hazard || '') + '" oninput="autoSave()"></div>'; - html += '<div class="form-group"><label><input type="checkbox" id="roomBlockTransport" onchange="autoSave()"' + (r.block_transport ? ' checked' : '') + '> Block Transport</label></div>'; + 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 = '<div class="panel-tabs">'; + html += '<button class="panel-tab' + (currentPanelTab === 0 ? ' active' : '') + '" onclick="switchPanelTab(0)">Room</button>'; + html += '<button class="panel-tab' + (currentPanelTab === 1 ? ' active' : '') + '" onclick="switchPanelTab(1)">Local</button>'; + html += '<button class="panel-tab' + (currentPanelTab === 2 ? ' active' : '') + '" onclick="switchPanelTab(2)">Objects</button>'; + html += '<button class="panel-tab' + (currentPanelTab === 3 ? ' active' : '') + '" onclick="switchPanelTab(3)">Mobs</button>'; + html += '<button class="panel-tab' + (currentPanelTab === 4 ? ' active' : '') + '" onclick="switchPanelTab(4)">Items</button>'; + html += '</div>'; + + html += '<div class="panel-tab-content">'; + 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); + html += '</div>'; panel.innerHTML = html; setTimeout(function() { @@ -331,7 +386,8 @@ function renderSidePanel(data) { loadRoomDirs(function(dirs) { var sel = $('#roomDir'); if (!sel) return; - var current = (file || '').replace(/^data\/rooms\/?/, '').split('/')[0] || ''; + var current = getRoomDirFromFile(file); + sel.innerHTML = ''; dirs.forEach(function(d) { var opt = document.createElement('option'); opt.value = d; @@ -343,6 +399,656 @@ function renderSidePanel(data) { }, 50); } +function renderRoomTab(r, file) { + var html = '<div class="form-group"><label>File</label><span style="font-size:11px;color:#888">' + esc(file) + '</span></div>'; + html += '<div class="form-group"><label>ID</label><input id="roomID" type="number" value="' + r.id + '" onchange="autoSave()"></div>'; + html += '<div class="form-group"><label>Name</label><input id="roomName" value="' + escAttr(r.name || '') + '" oninput="autoSave()"></div>'; + html += '<div class="form-group color-field"><label>Color</label><span class="color-swatch" style="background:'+resolveColor(r.color)+'"></span><input id="roomColor" value="' + escAttr(r.color || '') + '" oninput="autoSave()"></div>'; + html += '<div class="form-group"><label>Directory</label><select id="roomDir" onchange="autoSave()"></select></div>'; + 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 += '<div class="form-group"><label>Description</label><textarea id="roomDesc" rows="4" oninput="autoSave()">' + esc(descText) + '</textarea></div>'; + html += '<div class="form-group"><label>Hazard</label><input id="roomHazard" value="' + escAttr(r.hazard || '') + '" oninput="autoSave()"></div>'; + html += '<div class="form-group"><label><input type="checkbox" id="roomBlockTransport" onchange="autoSave()"' + (r.block_transport ? ' checked' : '') + '> Block Transport</label></div>'; + html += renderExitsSection(r); + return html; +} + +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 = renderLocalTab(localObjs); + else if (idx === 2) html = renderObjectsTab(refObjs); + else if (idx === 3) html = renderMobsTab(mobs); + else if (idx === 4) html = renderItemsTab(spawns); + 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 += '<div class="entry-row" data-idx="' + i + '">'; + h += '<div class="entry-fields">'; + h += '<input class="local-name" value="' + escAttr(obj.name || '') + '" placeholder="name" onchange="updateLocalObject(' + i + ')" style="font-weight:bold">'; + h += '<textarea class="local-desc" rows="2" placeholder="description (one per line)" onchange="updateLocalObject(' + i + ')">' + esc(descText) + '</textarea>'; + h += '<input class="local-aliases" value="' + escAttr((obj.aliases || []).join(', ')) + '" placeholder="aliases (comma separated)" onchange="updateLocalObject(' + i + ')">'; + h += '</div>'; + h += '<button class="btn btn-sm btn-danger" onclick="removeLocalObject(' + i + ')">X</button>'; + h += '</div>'; + }); + h += '<button class="btn btn-sm section-add-btn" onclick="addLocalObject()">+ Add Local Object</button>'; + return h; +} + +function renderObjectsTab(refObjs) { + var h = ''; + refObjs.forEach(function(obj, i) { + h += '<div class="entry-row" data-idx="' + i + '">'; + h += '<div class="entry-fields"><span class="entry-id">' + esc(obj.id || '') + '</span></div>'; + h += '<button class="btn btn-sm" onclick="openEditor(\'objects\', \'' + escAttr(obj.id || '') + '\')">Edit</button>'; + h += '<button class="btn btn-sm btn-danger" onclick="removeRefObject(' + i + ')">X</button>'; + h += '</div>'; + }); + h += '<div class="section-search">'; + h += '<input type="text" class="search-input" placeholder="Search objects..." oninput="searchRefObjects(this)">'; + h += '<div class="search-results" style="display:none"></div>'; + h += '</div>'; + return h; +} + +function renderMobsTab(mobs) { + var h = ''; + mobs.forEach(function(mob, i) { + h += '<div class="entry-row" data-idx="' + i + '">'; + h += '<div class="entry-fields"><span class="entry-id">' + esc(mob.id || '') + '</span></div>'; + h += '<button class="btn btn-sm" onclick="openEditor(\'mobs\', \'' + escAttr(mob.id || '') + '\')">Edit</button>'; + h += '<button class="btn btn-sm btn-danger" onclick="removeMob(' + i + ')">X</button>'; + h += '</div>'; + }); + h += '<div class="section-search">'; + h += '<input type="text" class="search-input" placeholder="Search mobs..." oninput="searchMobs(this)">'; + h += '<div class="search-results" style="display:none"></div>'; + h += '</div>'; + return h; +} + +function renderItemsTab(spawns) { + var h = ''; + spawns.forEach(function(spawn, i) { + h += '<div class="entry-row" data-idx="' + i + '">'; + h += '<div class="entry-fields">'; + h += '<span class="entry-id">' + esc(spawn.id || '') + '</span>'; + h += '<label class="mini-label">Qty</label><input class="mini-input spawn-qty" type="number" value="' + (spawn.quantity || 0) + '" onchange="updateSpawn(' + i + ')">'; + h += '<label class="mini-label">Respawn</label><input class="mini-input spawn-respawn" type="number" value="' + (spawn.respawn_ticks || 0) + '" onchange="updateSpawn(' + i + ')">'; + h += '</div>'; + h += '<button class="btn btn-sm" onclick="openEditor(\'items\', \'' + escAttr(spawn.id || '') + '\')">Edit</button>'; + h += '<button class="btn btn-sm btn-danger" onclick="removeSpawn(' + i + ')">X</button>'; + h += '</div>'; + }); + h += '<div class="section-search">'; + h += '<input type="text" class="search-input" placeholder="Search items..." oninput="searchItemSpawns(this)">'; + h += '<div class="search-results" style="display:none"></div>'; + h += '</div>'; + return h; +} + +function renderExitsSection(r) { + var exits = r.exits || {}; + var exitDirs = ['north','south','east','west','northeast','northwest','southeast','southwest','up','down']; + var dirAliases = {n:'north',s:'south',e:'east',w:'west',ne:'northeast',nw:'northwest',se:'southeast',sw:'southwest',u:'up',d:'down'}; + var h = '<div class="exit-section"><h3>Exits</h3>'; + 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; + h += '<div class="exit-row" data-dir="' + d + '">'; + h += '<span class="exit-dir">' + d + '</span>'; + h += '<span class="exit-target" onclick="selectRoom(' + target + ')"># ' + target + '</span>'; + if (bmsg) h += '<span class="exit-meta">Blocked: ' + esc(bmsg) + '</span>'; + if (cond) h += '<span class="exit-meta">Conditional</span>'; + h += '<button class="btn btn-sm" onclick="editExitCondition(\'' + d + '\')">Edit</button>'; + h += '<button class="btn btn-sm btn-danger" onclick="deleteExit(\'' + d + '\', ' + target + ')">X</button>'; + h += '</div>'; + }); + h += '<div class="exit-add">'; + h += '<select id="exitDirSelect">'; + exitDirs.forEach(function(d) { h += '<option value="' + d + '">' + d + '</option>'; }); + h += '</select>'; + h += '<input id="exitTargetID" type="number" placeholder="Target ID">'; + h += '<button class="exit-add-btn" onclick="addExit()">Add</button>'; + h += '</div>'; + h += '</div>'; + 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:420px;max-height:85vh;overflow-y:auto;padding:14px;z-index:201'; + + var h = '<h3 style="margin-bottom:8px;font-size:13px">Exit: ' + esc(dir) + ' → #' + exit.room + '</h3>'; + h += '<div class="form-group"><label>Blocked Message</label><textarea id="exitBmsg" rows="2">' + esc(bmsg) + '</textarea></div>'; + h += '<div class="form-group"><label>Set Flags (key=value, comma separated)</label><input id="exitFlags" value="' + escAttr(flagsStr) + '"></div>'; + h += '<div class="form-group"><label>Set Player Flags (key=value, comma separated)</label><input id="exitPFlags" value="' + escAttr(pflagsStr) + '"></div>'; + + h += '<div style="margin-top:10px;border-top:1px solid var(--border);padding-top:8px">'; + h += '<label style="font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.5px">Conditions</label>'; + h += '<div id="condClauses" style="margin-top:4px"></div>'; + h += '<button class="btn btn-sm section-add-btn" onclick="addConditionClause()">+ Add Condition</button>'; + h += '<div class="btn-row" style="margin-top:10px">'; + h += '<button class="btn btn-primary" onclick="saveExitCondition()">Save</button>'; + h += '<button class="btn btn-danger" onclick="clearExitCondition()">Clear</button>'; + h += '<button class="btn" onclick="closeExitModal()">Cancel</button>'; + h += '</div></div>'; + + menu.innerHTML = h; + document.body.appendChild(overlay); + document.body.appendChild(menu); + overlay.onclick = closeExitModal; + + menu._condClauses = parsed.clauses; + menu._condMode = parsed.mode; + // Fix: mode is always 'all' for our editor, but store what was parsed + 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 += '<div class="cond-clause" data-idx="' + i + '" style="display:flex;gap:4px;align-items:center;margin-bottom:4px">'; + h += '<select class="cond-type" onchange="var c=this.parentElement;var m=this.closest(\'.exit-modal\');var idx=parseInt(c.getAttribute(\'data-idx\'));m._condClauses[idx].type=this.value;renderConditionClauses(m)">'; + ['player_flag','flag','value','has_item','min_credits'].forEach(function(t) { + h += '<option value="' + t + '"' + (clause.type === t ? ' selected' : '') + '>' + t + '</option>'; + }); + h += '</select>'; + if (clause.type === 'value') { + var kv = clause.value ? clause.value.split('=', 2) : ['','']; + var kn = (kv[0] || '').trim(); + var kvv = (kv[1] || '').trim(); + h += '<input class="cond-val-key" placeholder="key" value="' + escAttr(kn) + '" onchange="updateClauseData(this)" style="width:80px">'; + h += '<span style="color:#888">=</span>'; + h += '<input class="cond-val-val" placeholder="value" value="' + escAttr(kvv) + '" onchange="updateClauseData(this)" style="flex:1">'; + } else { + h += '<input class="cond-val" value="' + escAttr(clause.value || '') + '" onchange="updateClauseData(this)" style="flex:1">'; + } + h += '<label style="font-size:10px;color:#aaa;white-space:nowrap"><input type="checkbox" class="cond-not"' + (clause.not ? ' checked' : '') + ' onchange="updateClauseData(this)"> not</label>'; + h += '<button class="btn btn-sm btn-danger" onclick="removeConditionClause(this)" style="flex-shrink:0">X</button>'; + h += '</div>'; + }); + 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 : ''); + + // Update clause data from DOM before saving + 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 = {not: !!clause.not}; + 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(); + 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(); + 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; + autoSave(); +} + +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; + autoSave(); +} + +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; + autoSave(); +} + +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'); + 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 = []; + } + 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 (items.length === 0) { + results.style.display = 'block'; + results.innerHTML = '<div class="sr-item" style="color:#888">No results</div>'; + return; + } + results.style.display = 'block'; + results.innerHTML = items.map(function(r) { + return '<div class="sr-item" onclick="event.stopPropagation(); selectSearchResult(this)">' + esc(r.id) + ' <span style="color:#aaa">' + esc(r.name || '') + '</div>'; + }).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); @@ -351,6 +1057,12 @@ function autoSave() { function doSavePanel() { var id = selectedRoom; if (!id) return; + var save = function(r) { + API.put('/api/rooms/' + id, r).then(function() { + updateUndoBar(); + }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); + }; + API.get('/api/rooms/' + id).then(function(data) { var r = data.room || {}; r.id = id; @@ -360,29 +1072,30 @@ function doSavePanel() { el = $('#roomHazard'); if (el) r.hazard = 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; + } + 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(); }); + }).catch(function(e) { notify('Rename failed: '+e.message, 'error'); save(r); }); + } else { save(r); } + }).catch(function(e) { notify('Move failed: '+e.message, 'error'); save(r); }); } 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(); } + }).catch(function(e) { notify('Rename failed: '+e.message, 'error'); save(r); }); + } else { save(r); } }); } @@ -699,20 +1412,22 @@ function clearDelHighlight() { } function deleteLink(fromId, toId) { + var orphanedRooms = []; 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; + orphanedRooms.push(roomId); } - return true; }; - if (!checkOrphan(toId, fromId)) return; - if (!checkOrphan(fromId, toId)) return; + 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', { @@ -724,19 +1439,75 @@ function deleteLink(fromId, toId) { if (!r.ok) throw new Error('unlink failed'); return r.json(); }).then(function() { - notify('Removed link #'+fromId+' '+toId, 'success'); + 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'); updateUndoBar(); - loadMap(); - if (selectedRoom) { - API.get('/api/rooms/' + selectedRoom).then(function(data) { - renderSidePanel(data); - }); + if (orphanedRooms.indexOf(selectedRoom) !== -1) { + selectedRoom = null; + $('#panelContent').innerHTML = '<p style="color:#888;text-align:center;margin-top:40px">Room deleted</p>'; } + 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 '<div class="dc-item" onclick="selectRoom(' + r.id + ')" title="' + esc(r.name || '') + '">' + + '<span class="dc-id">#' + r.id + '</span>' + + '<span class="dc-name">' + esc(r.name || '') + '</span>' + + '</div>'; + }).join(''); +} + +function initPanelResize() { + var handle = document.createElement('div'); + handle.className = 'panel-resize-handle'; + var panel = $('#sidePanel'); + if (!panel) return; + panel.appendChild(handle); + 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'; + } + function onUp() { + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + } +} + function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); @@ -772,4 +1543,4 @@ function updateDirSelect() { } } -document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); }); +document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); initPanelResize(); }); |
