From d9ee1508a13521939187144065edf08e3ddd21c8 Mon Sep 17 00:00:00 2001 From: Volte6 <143822+Volte6@users.noreply.github.com> Date: Mon, 5 May 2025 15:56:33 -0700 Subject: [PATCH 1/6] Sending a specially wrapped payload for GMCP over websocket. --- _datafiles/html/public/webclient-pure.html | 74 +++++++++++++++++++++- modules/gmcp/gmcp.go | 39 +++++++++--- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/_datafiles/html/public/webclient-pure.html b/_datafiles/html/public/webclient-pure.html index 175d9857..79104014 100644 --- a/_datafiles/html/public/webclient-pure.html +++ b/_datafiles/html/public/webclient-pure.html @@ -197,6 +197,7 @@

Volume Controls

+ @@ -247,7 +248,59 @@

Volume Controls

// ///////////////////////////////////////////// let GMCPStructs = {}; - let GMCPUpdateHandlers = {}; + let GMCPUpdateHandlers = { + "Char":function() { + + var obj = GMCPStructs["Char"]; + + if ( obj.Vitals ) { + GMCPUpdateHandlers['Char.Vitals'](); + } + }, + "Char.Vitals": function() { + + var obj = GMCPStructs["Char"]; + if ( !obj.Vitals ) { + return; + } + + if ( !GMCPWindows['Char.Vitals'] && GMCPWindows['Char.Vitals'] !== false ) { + + GMCPWindows['Char.Vitals'] = new WinBox({ title: "Vitals", + mount: document.getElementById("vitals-bars"), + background: "#1c6b60", + border: 1, + width:300, + height:20+120, + header: 20, + onclose: force => { GMCPWindows['Char.Vitals'] = false; return false; }, + oncreate: opts => { + opts.width = document.getElementById('vitals-bars').style.width; + opts.height = "48px"; + } + + }); + } + + if ( GMCPWindows['Char.Vitals'] === false ) { + return; + } + + var p = Math.max(0, Math.min(100, Math.floor(obj.Vitals.hp/obj.Vitals.hp_max*100))); // clamp 0–100 + var fill = document.querySelector('#health-bar .health-fill'); + var txt = document.querySelector('#health-bar .health-text'); + fill.style.width = 100-p + '%'; + txt.textContent = obj.Vitals.hp + '/' + obj.Vitals.hp_max + ' hp'; + + p = Math.max(0, Math.min(100, Math.floor(obj.Vitals.sp/obj.Vitals.sp_max*100))); // clamp 0–100 + fill = document.querySelector('#mana-bar .mana-fill'); + txt = document.querySelector('#mana-bar .mana-text'); + fill.style.width = 100-p + '%'; + txt.textContent = obj.Vitals.sp + '/' + obj.Vitals.sp_max + ' mp'; + + }, + }; + let GMCPWindows = {}; function handleGMCP(objNamespace, objBody) { @@ -268,12 +321,11 @@

Volume Controls

} cursor[lastProperty] = objBody; - nameParts = objNamespace.split("."); for ( let i=nameParts.length-1; i>=0; i-- ) { var path = nameParts.slice(0, i+1).join("."); if ( GMCPUpdateHandlers[path] ) { - GMCPUpdateHandlers[path](GMCPStructs[rootName]); + GMCPUpdateHandlers[path](); return; } } @@ -803,5 +855,101 @@

Volume Controls

} } + + +
+
+
+
+ 100% +
+
+
+ 100% +
+
+
From fcc5ca758794ba4a4a34a78920013b1f07381ec4 Mon Sep 17 00:00:00 2001 From: Volte6 <143822+Volte6@users.noreply.github.com> Date: Tue, 6 May 2025 09:48:33 -0700 Subject: [PATCH 3/6] experimenting with mapping --- _datafiles/html/public/static/js/gmcp.js | 247 +++++++++++++++++++++ _datafiles/html/public/webclient-pure.html | 63 ++++++ 2 files changed, 310 insertions(+) create mode 100644 _datafiles/html/public/static/js/gmcp.js diff --git a/_datafiles/html/public/static/js/gmcp.js b/_datafiles/html/public/static/js/gmcp.js new file mode 100644 index 00000000..b0985ac9 --- /dev/null +++ b/_datafiles/html/public/static/js/gmcp.js @@ -0,0 +1,247 @@ + +class GraphRenderer { + constructor({ container, onRoomClick }) { + this.rooms = new Map(); + this.onRoomClick = onRoomClick; + + // build SVG & viewport + this.svg = document.createElementNS("http://www.w3.org/2000/svg","svg"); + this.svg.setAttribute("width","100%"); + this.svg.setAttribute("height","100%"); + this.svg.style.cursor = "grab"; + container.appendChild(this.svg); + + this.viewport = document.createElementNS(this.svg.namespaceURI,"g"); + this.svg.appendChild(this.viewport); + + // pan/zoom state + this.tx = 0; this.ty = 0; this.k = 1; + this._initPanZoom(); + this._initClick(); + } + + // ——— pan & zoom (unchanged) ——— + _initPanZoom() { + this.svg.addEventListener("pointerdown", e => { + this.svg.setPointerCapture(e.pointerId); + this.isPanning = true; + this.panStart = { x: e.clientX, y: e.clientY }; + this.transStart = { tx: this.tx, ty: this.ty }; + this.svg.style.cursor = "grabbing"; + }); + this.svg.addEventListener("pointermove", e => { + if (!this.isPanning) return; + const dx = (e.clientX - this.panStart.x) / this.k; + const dy = (e.clientY - this.panStart.y) / this.k; + this.tx = this.transStart.tx + dx; + this.ty = this.transStart.ty + dy; + this._updateTransform(); + }); + this.svg.addEventListener("pointerup", e => { + this.svg.releasePointerCapture(e.pointerId); + this.isPanning = false; + this.svg.style.cursor = "grab"; + }); + this.svg.addEventListener("wheel", e => { + e.preventDefault(); + const zoomSpeed = 0.002; + const scale = Math.exp(-e.deltaY * zoomSpeed); + const { width, height } = this.svg.getBoundingClientRect(); + const pt = this.svg.createSVGPoint(); + pt.x = width/2; pt.y = height/2; + const before = pt.matrixTransform(this.viewport.getCTM().inverse()); + this.k *= scale; + this._updateTransform(); + const after = pt.matrixTransform(this.viewport.getCTM().inverse()); + this.tx += (after.x - before.x); + this.ty += (after.y - before.y); + this._updateTransform(); + }, { passive: false }); + } + + _initClick() { + this.viewport.addEventListener("click", e => { + const hit = e.target.closest("[data-room-id]"); + if (hit) this.onRoomClick(+hit.dataset.roomId); + }); + } + + _updateTransform() { + this.viewport.setAttribute( + "transform", + `translate(${this.tx},${this.ty}) scale(${this.k})` + ); + } + + _recomputeBounds() { + if (!this.rooms.size) return; + const xs = [], ys = []; + for (let r of this.rooms.values()) { + xs.push(r.x); ys.push(r.y); + } + const minX = Math.min(...xs), maxX = Math.max(...xs); + const minY = Math.min(...ys), maxY = Math.max(...ys); + const w = maxX - minX + 2, h = maxY - minY + 2; + this.svg.setAttribute("viewBox", + `${minX-1} ${minY-1} ${w} ${h}` + ); + } + + _draw() { + while (this.viewport.firstChild) + this.viewport.removeChild(this.viewport.firstChild); + + // edges + for (let r of this.rooms.values()) { + for (let dir in r.Exits) { + const toId = Number(r.Exits[dir].RoomId); + const tgt = this.rooms.get(toId); + if (!tgt) continue; + const line = document.createElementNS(this.svg.namespaceURI,"line"); + line.setAttribute("x1", r.x); + line.setAttribute("y1", r.y); + line.setAttribute("x2", tgt.x); + line.setAttribute("y2", tgt.y); + line.setAttribute("stroke","#444"); + line.setAttribute("stroke-width","0.02"); + this.viewport.appendChild(line); + } + } + + // rooms + for (let r of this.rooms.values()) { + const g = document.createElementNS(this.svg.namespaceURI,"g"); + g.setAttribute("data-room-id", r.RoomId); + + const rect = document.createElementNS(this.svg.namespaceURI,"rect"); + rect.setAttribute("x", r.x - 0.4); + rect.setAttribute("y", r.y - 0.4); + rect.setAttribute("width","0.8"); + rect.setAttribute("height","0.8"); + rect.setAttribute("fill","white"); + rect.setAttribute("stroke","#333"); + rect.style.cursor = "pointer"; + g.appendChild(rect); + + const text = document.createElementNS(this.svg.namespaceURI,"text"); + text.setAttribute("x", r.x); + text.setAttribute("y", r.y + 0.1); + text.setAttribute("text-anchor","middle"); + text.setAttribute("font-size","0.4"); + text.textContent = r.RoomId; + text.style.pointerEvents = "none"; + g.appendChild(text); + + this.viewport.appendChild(g); + } + } + + // ——— collision‐free full‐graph layout ——— + _layoutAll() { + if (!this.rooms.size) return; + + // clear coords & init occupied set + const occupied = new Set(); + for (let r of this.rooms.values()) { + r.x = undefined; r.y = undefined; + } + + // seed root at (0,0) + const root = this.rooms.values().next().value; + root.x = 0; root.y = 0; + occupied.add("0,0"); + + const visited = new Set([ root.RoomId ]); + const queue = [ root.RoomId ]; + + // direction vectors scaled + const S = this.cellSize; + const DIR = { + north: [ 0, -S ], + south: [ 0, S ], + east: [ S, 0 ], + west: [ -S, 0 ] + }; + + // BFS assign + while (queue.length) { + const id = queue.shift(); + const room = this.rooms.get(id); + + for (let dir in room.Exits) { + const toId = Number(room.Exits[dir].RoomId); + if (visited.has(toId)) continue; + const nbr = this.rooms.get(toId); + if (!nbr) continue; + + // start one cell over in the desired direction + let [dx, dy] = DIR[dir] || [S, 0]; + let cx = room.x + dx, + cy = room.y + dy; + + // walk further if occupied + while (occupied.has(`${cx},${cy}`)) { + cx += dx; + cy += dy; + } + + nbr.x = cx; nbr.y = cy; + occupied.add(`${cx},${cy}`); + + visited.add(toId); + queue.push(toId); + } + } + + // fallback for any unconnected rooms + const placed = Array.from(visited).map(i => this.rooms.get(i)); + let maxX = Math.max(...placed.map(r => r.x)); + const minY = Math.min(...placed.map(r => r.y)); + + for (let [id, r] of this.rooms) { + if (visited.has(id)) continue; + let cx = maxX + S, cy = minY; + while (occupied.has(`${cx},${cy}`)) { + cx += S; + } + r.x = cx; r.y = cy; + occupied.add(`${cx},${cy}`); + maxX = cx; + } + } + + + // ——— Public API ——— + + setRooms(arr) { + this.rooms.clear(); + for (let r of arr) { + r.RoomId = Number(r.RoomId); + this.rooms.set(r.RoomId, r); + } + this._layoutAll(); + this._recomputeBounds(); + this._draw(); + } + + addRoom(room) { + room.RoomId = Number(room.RoomId); + this.rooms.set(room.RoomId, room); + + this._layoutAll(); + this._recomputeBounds(); + this._draw(); + } + + centerOn(roomId) { + const r = this.rooms.get(Number(roomId)); + if (!r) return; + const [ vx, vy, vw, vh ] = this.svg + .getAttribute("viewBox") + .split(/[\s,]+/).map(Number); + const cx = vx + vw/2, cy = vy + vh/2; + this.tx = cx/this.k - r.x; + this.ty = cy/this.k - r.y; + this._updateTransform(); + } +} diff --git a/_datafiles/html/public/webclient-pure.html b/_datafiles/html/public/webclient-pure.html index e6fe4bb0..c2b39f34 100644 --- a/_datafiles/html/public/webclient-pure.html +++ b/_datafiles/html/public/webclient-pure.html @@ -166,6 +166,7 @@ + @@ -248,7 +249,67 @@

Volume Controls

// ///////////////////////////////////////////// let GMCPStructs = {}; + let gr = {}; + let allRooms = {}; let GMCPUpdateHandlers = { + "Room": function() { + + var obj = GMCPStructs["Room"]; + if ( !obj.Info ) { + return; + } + + if ( !GMCPWindows['Map'] && GMCPWindows['Map'] !== false ) { + + + + GMCPWindows['Map'] = new WinBox({ title: "Map", + mount: document.getElementById("map-render"), + background: "#1c6b60", + border: 1, + width:600, + height:20+600, + header: 20, + onclose: force => { GMCPWindows['Map'] = false; return false; }, + oncreate: ops => { + gr = new GraphRenderer({ + container: document.getElementById('map-render'), + onRoomClick: (graph, id) => { + graph.centerOn(id); + } + }); + }, + }); + + + } + + + if ( GMCPWindows['Map'] === false ) { + return; + } + + coords = obj.Info.coords.split(","); + var r = { RoomId: obj.Info.num, x: parseInt(coords[1]), y: parseInt(coords[2]), Exits: {} } + for (var p in obj.Info.exits ) { + r.Exits[p] = { RoomId: obj.Info.exits[p].num }; + } + + if ( !allRooms[r.RoomId] ) { + + allRooms[r.RoomId] = r; + var rArr = []; + for( var o in allRooms ) { + rArr.push(allRooms[o]); + } + gr.setRooms(rArr); + + } + + // pan/zoom will feel smooth; to recenter on room 2: + gr.centerOn(r.RoomId); + + }, "Char":function() { var obj = GMCPStructs["Char"]; @@ -950,6 +1011,8 @@

Volume Controls

100% + +
From 44e61fee8cdce93fbdd94d9318541fe356194860 Mon Sep 17 00:00:00 2001 From: Volte6 <143822+Volte6@users.noreply.github.com> Date: Tue, 6 May 2025 10:48:40 -0700 Subject: [PATCH 4/6] excluding winbox from lint check --- .jshintignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.jshintignore b/.jshintignore index 56c80727..8c4a12b2 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,3 +1,5 @@ _datafiles/html/public/static/js/xterm.4.19.0.js _datafiles/html/public/static/js/xterm-addon-fit.js -_datafiles/html/admin/static/js/htmx.2.0.3.js \ No newline at end of file +_datafiles/html/public/static/js/winbox.bundle.min.js +_datafiles/html/admin/static/js/htmx.2.0.3.js + From e25b695a828fd5527a34c3527b13bbc7b55fd016 Mon Sep 17 00:00:00 2001 From: Volte6 <143822+Volte6@users.noreply.github.com> Date: Tue, 6 May 2025 13:54:37 -0700 Subject: [PATCH 5/6] basic mapping using gmcp --- _datafiles/html/public/static/js/gmcp.js | 507 ++++++++++++--------- _datafiles/html/public/webclient-pure.html | 134 ++++-- 2 files changed, 386 insertions(+), 255 deletions(-) diff --git a/_datafiles/html/public/static/js/gmcp.js b/_datafiles/html/public/static/js/gmcp.js index b0985ac9..c61f6e15 100644 --- a/_datafiles/html/public/static/js/gmcp.js +++ b/_datafiles/html/public/static/js/gmcp.js @@ -1,247 +1,316 @@ +class RoomGridSVG { + constructor(selector, options = {}) { + // ── Configurable options & defaults ─────────────────────────────── + this.cellSize = options.cellSize || 100 + this.cellMargin = options.cellMargin || 20 + this.spacing = this.cellSize + this.cellMargin + this.zoomStep = options.zoomStep || 1.2 + this.zoomLevel = options.initialZoom || 1 + this.onRoomClick = options.onRoomClick || (() => {}) + this.zoomButtonSize = options.zoomButtonSize || 25 + this.controlsMargin = options.controlsMargin || 10 + this.roomEdgeColor = options.roomEdgeColor || "#1c6b60" + this.visitingColor = options.visitingColor || "#c20000" + // ── Internal state ──────────────────────────────────────────────── + // rooms: Map + this.rooms = new Map() + this.drawnEdges = new Set() // to avoid dup lines + this.currentCenterId = null // for highlight -class GraphRenderer { - constructor({ container, onRoomClick }) { - this.rooms = new Map(); - this.onRoomClick = onRoomClick; - - // build SVG & viewport - this.svg = document.createElementNS("http://www.w3.org/2000/svg","svg"); - this.svg.setAttribute("width","100%"); - this.svg.setAttribute("height","100%"); - this.svg.style.cursor = "grab"; - container.appendChild(this.svg); - - this.viewport = document.createElementNS(this.svg.namespaceURI,"g"); - this.svg.appendChild(this.viewport); - - // pan/zoom state - this.tx = 0; this.ty = 0; this.k = 1; - this._initPanZoom(); - this._initClick(); - } + // ── Build container & SVG ───────────────────────────────────────── + this.container = document.querySelector(selector) + this.container.style.position = 'relative' + + this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg') + this.svg.setAttribute('preserveAspectRatio','xMidYMid meet') + this.svg.style.width = '100%' + this.svg.style.height = '100%' + this.container.appendChild(this.svg) + + // Connections under rooms: + this.connectionsGroup = document.createElementNS(this.svg.namespaceURI,'g') + this.svg.appendChild(this.connectionsGroup) + // Rooms on top: + this.roomsGroup = document.createElementNS(this.svg.namespaceURI,'g') + this.svg.appendChild(this.roomsGroup) + + // Default tiny viewBox until rooms exist: + this.svg.setAttribute('viewBox','0 0 1 1') - // ——— pan & zoom (unchanged) ——— - _initPanZoom() { - this.svg.addEventListener("pointerdown", e => { - this.svg.setPointerCapture(e.pointerId); - this.isPanning = true; - this.panStart = { x: e.clientX, y: e.clientY }; - this.transStart = { tx: this.tx, ty: this.ty }; - this.svg.style.cursor = "grabbing"; - }); - this.svg.addEventListener("pointermove", e => { - if (!this.isPanning) return; - const dx = (e.clientX - this.panStart.x) / this.k; - const dy = (e.clientY - this.panStart.y) / this.k; - this.tx = this.transStart.tx + dx; - this.ty = this.transStart.ty + dy; - this._updateTransform(); - }); - this.svg.addEventListener("pointerup", e => { - this.svg.releasePointerCapture(e.pointerId); - this.isPanning = false; - this.svg.style.cursor = "grab"; - }); - this.svg.addEventListener("wheel", e => { - e.preventDefault(); - const zoomSpeed = 0.002; - const scale = Math.exp(-e.deltaY * zoomSpeed); - const { width, height } = this.svg.getBoundingClientRect(); - const pt = this.svg.createSVGPoint(); - pt.x = width/2; pt.y = height/2; - const before = pt.matrixTransform(this.viewport.getCTM().inverse()); - this.k *= scale; - this._updateTransform(); - const after = pt.matrixTransform(this.viewport.getCTM().inverse()); - this.tx += (after.x - before.x); - this.ty += (after.y - before.y); - this._updateTransform(); - }, { passive: false }); + // ── HTML overlay zoom controls ──────────────────────────────────── + this._createHTMLControls() } - _initClick() { - this.viewport.addEventListener("click", e => { - const hit = e.target.closest("[data-room-id]"); - if (hit) this.onRoomClick(+hit.dataset.roomId); - }); + // ── Public API ─────────────────────────────────────────────────────── + + /** + * Add or update a room. + * - Pre-adds any Exits given as {RoomId,x,y,…} + * - If room already exists, updates its position, color, text, & redraws edges. + */ + addRoom(room) { + const id = room.RoomId + + // 1) Pre-add exit-defined rooms + if (Array.isArray(room.Exits)) { + room.Exits.forEach(e => { + if (e && typeof e==='object' && e.RoomId != null) { + + if (this.rooms.has(e.RoomId)) return; + + this.addRoom({ + RoomId: e.RoomId, + Text: e.Text != null?e.Text:String(e.RoomId), + x: e.x, y: e.y, + Exits: Array.isArray(e.Exits)?e.Exits:[] + }) + } + }) + } + + // prepare defaults + const defaultColor = room.Color || '#fff' + const displayText = room.Text != null + ? room.Text + : String(room.RoomId) + + // 2) UPDATE existing + if (this.rooms.has(id)) { + const entry = this.rooms.get(id) + // update stored data + entry.room.x = room.x + entry.room.y = room.y + entry.room.Exits = Array.isArray(room.Exits)?room.Exits:[] + entry.room.Color = room.Color + entry.room.Text = room.Text + entry.defaultColor = defaultColor + + // move & recolor rect + const rect = this.svg.querySelector(`rect[data-room-rect="${id}"]`) + rect.setAttribute('x', room.x * this.spacing) + rect.setAttribute('y', room.y * this.spacing) + if (this.currentCenterId === id) { + rect.setAttribute('fill', this.visitingColor) + } else { + rect.setAttribute('fill', defaultColor) + } + + // move & update label + const txtEl = this.svg.querySelector(`g[data-room-id="${id}"] text`) + txtEl.setAttribute('x', room.x * this.spacing + this.cellSize/2) + txtEl.setAttribute('y', room.y * this.spacing + this.cellSize/2 + 5) + txtEl.textContent = displayText + + // redraw any new edges + this._drawEdgesForRoom(id) + + // refresh bounds & view + this._updateBounds() + this._applyZoom() + return + } + + // 3) NEW room → draw group + const g = document.createElementNS(this.svg.namespaceURI,'g') + g.setAttribute('data-room-id', id) + + // square + const rect = document.createElementNS(this.svg.namespaceURI,'rect') + rect.setAttribute('width', this.cellSize) + rect.setAttribute('height', this.cellSize) + rect.setAttribute('x', room.x * this.spacing) + rect.setAttribute('y', room.y * this.spacing) + rect.setAttribute('stroke', this.roomEdgeColor) + rect.setAttribute('stroke-width','4') + rect.setAttribute('rx', this.cellSize/10); // corner radius X + rect.setAttribute('ry', this.cellSize/10); // corner radius Y + rect.setAttribute('data-room-rect', id) + rect.setAttribute('fill', defaultColor) + rect.style.cursor = 'pointer' + rect.addEventListener('click', () => this.onRoomClick(room)) + g.appendChild(rect) + + // label + const label = document.createElementNS(this.svg.namespaceURI,'text') + label.setAttribute('x', room.x * this.spacing + this.cellSize/2) + label.setAttribute('y', room.y * this.spacing + this.cellSize/2 + 5) + label.setAttribute('text-anchor', 'middle') + label.setAttribute('font-size', this.cellSize * 0.3) + label.textContent = displayText + g.appendChild(label) + + this.roomsGroup.appendChild(g) + this.rooms.set(id, { room, group: g, defaultColor }) + + // draw edges for this new room + this._drawEdgesForRoom(id) + + // refresh bounds & view + this._updateBounds() + this._applyZoom() } - _updateTransform() { - this.viewport.setAttribute( - "transform", - `translate(${this.tx},${this.ty}) scale(${this.k})` - ); + /** + * Bulk‐set rooms (wipes existing). + */ + setRooms(arr) { + this.reset() + arr.forEach(r => this.addRoom(r)) } - _recomputeBounds() { - if (!this.rooms.size) return; - const xs = [], ys = []; - for (let r of this.rooms.values()) { - xs.push(r.x); ys.push(r.y); - } - const minX = Math.min(...xs), maxX = Math.max(...xs); - const minY = Math.min(...ys), maxY = Math.max(...ys); - const w = maxX - minX + 2, h = maxY - minY + 2; - this.svg.setAttribute("viewBox", - `${minX-1} ${minY-1} ${w} ${h}` - ); + /** + * Clear everything. + */ + reset() { + this.rooms.clear() + this.drawnEdges.clear() + this.currentCenterId = null + this.zoomLevel = 1 + this.svg.setAttribute('viewBox','0 0 1 1') + this.roomsGroup.innerHTML = '' + this.connectionsGroup.innerHTML = '' } - _draw() { - while (this.viewport.firstChild) - this.viewport.removeChild(this.viewport.firstChild); - - // edges - for (let r of this.rooms.values()) { - for (let dir in r.Exits) { - const toId = Number(r.Exits[dir].RoomId); - const tgt = this.rooms.get(toId); - if (!tgt) continue; - const line = document.createElementNS(this.svg.namespaceURI,"line"); - line.setAttribute("x1", r.x); - line.setAttribute("y1", r.y); - line.setAttribute("x2", tgt.x); - line.setAttribute("y2", tgt.y); - line.setAttribute("stroke","#444"); - line.setAttribute("stroke-width","0.02"); - this.viewport.appendChild(line); + /** + * Center & highlight a room. Previous one reverts to its default color. + */ + centerOnRoom(id) { + const entry = this.rooms.get(id) + if (!entry) return + + // un-highlight previous + if (this.currentCenterId != null) { + const prevRect = this.svg.querySelector( + `rect[data-room-rect="${this.currentCenterId}"]` + ) + if (prevRect) { + const prevEntry = this.rooms.get(this.currentCenterId) + prevRect.setAttribute('fill', prevEntry.defaultColor) } } - // rooms - for (let r of this.rooms.values()) { - const g = document.createElementNS(this.svg.namespaceURI,"g"); - g.setAttribute("data-room-id", r.RoomId); - - const rect = document.createElementNS(this.svg.namespaceURI,"rect"); - rect.setAttribute("x", r.x - 0.4); - rect.setAttribute("y", r.y - 0.4); - rect.setAttribute("width","0.8"); - rect.setAttribute("height","0.8"); - rect.setAttribute("fill","white"); - rect.setAttribute("stroke","#333"); - rect.style.cursor = "pointer"; - g.appendChild(rect); - - const text = document.createElementNS(this.svg.namespaceURI,"text"); - text.setAttribute("x", r.x); - text.setAttribute("y", r.y + 0.1); - text.setAttribute("text-anchor","middle"); - text.setAttribute("font-size","0.4"); - text.textContent = r.RoomId; - text.style.pointerEvents = "none"; - g.appendChild(text); - - this.viewport.appendChild(g); + // compute new view center + this.center = { + x: entry.room.x * this.spacing + this.cellSize/2, + y: entry.room.y * this.spacing + this.cellSize/2 } + this._applyZoom() + + // highlight new + const newRect = this.svg.querySelector( + `rect[data-room-rect="${id}"]` + ) + if (newRect) newRect.setAttribute('fill', this.visitingColor) + + this.currentCenterId = id } - // ——— collision‐free full‐graph layout ——— - _layoutAll() { - if (!this.rooms.size) return; - - // clear coords & init occupied set - const occupied = new Set(); - for (let r of this.rooms.values()) { - r.x = undefined; r.y = undefined; - } - - // seed root at (0,0) - const root = this.rooms.values().next().value; - root.x = 0; root.y = 0; - occupied.add("0,0"); - - const visited = new Set([ root.RoomId ]); - const queue = [ root.RoomId ]; - - // direction vectors scaled - const S = this.cellSize; - const DIR = { - north: [ 0, -S ], - south: [ 0, S ], - east: [ S, 0 ], - west: [ -S, 0 ] - }; - - // BFS assign - while (queue.length) { - const id = queue.shift(); - const room = this.rooms.get(id); - - for (let dir in room.Exits) { - const toId = Number(room.Exits[dir].RoomId); - if (visited.has(toId)) continue; - const nbr = this.rooms.get(toId); - if (!nbr) continue; - - // start one cell over in the desired direction - let [dx, dy] = DIR[dir] || [S, 0]; - let cx = room.x + dx, - cy = room.y + dy; - - // walk further if occupied - while (occupied.has(`${cx},${cy}`)) { - cx += dx; - cy += dy; - } - - nbr.x = cx; nbr.y = cy; - occupied.add(`${cx},${cy}`); - - visited.add(toId); - queue.push(toId); - } + zoomIn() { this.zoomLevel *= this.zoomStep; this._applyZoom() } + zoomOut() { this.zoomLevel /= this.zoomStep; this._applyZoom() } + + drawConnection(a, b) { + if (!this.rooms.has(a) || !this.rooms.has(b)) return + this._drawEdge(a, b) + this._applyZoom() + } + + // ── Private draw helpers ─────────────────────────────────────────────── + + _createHTMLControls() { + const div = document.createElement('div') + div.style.cssText = ` + position:absolute; + top:${this.controlsMargin}px; + right:${this.controlsMargin}px; + display:flex; gap:5px; + ` + const mk = (lbl, cb) => { + const b = document.createElement('button') + b.textContent = lbl + b.style.cssText = ` + width:${this.zoomButtonSize}px; + height:${this.zoomButtonSize}px; + font-size:${this.zoomButtonSize*0.6}px; + line-height:1; + ` + b.addEventListener('click', cb) + return b } - - // fallback for any unconnected rooms - const placed = Array.from(visited).map(i => this.rooms.get(i)); - let maxX = Math.max(...placed.map(r => r.x)); - const minY = Math.min(...placed.map(r => r.y)); - - for (let [id, r] of this.rooms) { - if (visited.has(id)) continue; - let cx = maxX + S, cy = minY; - while (occupied.has(`${cx},${cy}`)) { - cx += S; + div.append(mk('−',()=>this.zoomOut()), mk('+',()=>this.zoomIn())) + this.container.appendChild(div) + } + + _drawEdgesForRoom(id) { + const me = this.rooms.get(id).room + const exits = Array.isArray(me.Exits)?me.Exits:[] + + // draw its own exits + exits.forEach(e => { + const to = (typeof e==='object')?e.RoomId:e + if (this.rooms.has(to)) this._drawEdge(id, to) + }) + + // draw others’ exits back to it + this.rooms.forEach(({ room }, otherId) => { + if (otherId===id) return + const oe = Array.isArray(room.Exits)?room.Exits:[] + if (oe.some(x => ((typeof x==='object')?x.RoomId:x)===id)) { + this._drawEdge(otherId, id) } - r.x = cx; r.y = cy; - occupied.add(`${cx},${cy}`); - maxX = cx; - } + }) } - - // ——— Public API ——— + _drawEdge(a, b) { + const key = ae.room.x) + const ys = [...this.rooms.values()].map(e=>e.room.y) + this.bounds = { + minX: Math.min(...xs), + maxX: Math.max(...xs), + minY: Math.min(...ys), + maxY: Math.max(...ys) + } + } + this.worldWidth = (this.bounds.maxX - this.bounds.minX + 1)*this.spacing + this.worldHeight = (this.bounds.maxY - this.bounds.minY + 1)*this.spacing - this._layoutAll(); - this._recomputeBounds(); - this._draw(); + if (!this.center && this.rooms.size) { + this.center = { + x: this.bounds.minX*this.spacing + this.worldWidth/2, + y: this.bounds.minY*this.spacing + this.worldHeight/2 + } + } } - centerOn(roomId) { - const r = this.rooms.get(Number(roomId)); - if (!r) return; - const [ vx, vy, vw, vh ] = this.svg - .getAttribute("viewBox") - .split(/[\s,]+/).map(Number); - const cx = vx + vw/2, cy = vy + vh/2; - this.tx = cx/this.k - r.x; - this.ty = cy/this.k - r.y; - this._updateTransform(); + _applyZoom() { + const hw = this.worldWidth / (2*this.zoomLevel) + const hh = this.worldHeight / (2*this.zoomLevel) + const x0 = (this.center?.x||this.worldWidth/2) - hw + const y0 = (this.center?.y||this.worldHeight/2) - hh + this.svg.setAttribute('viewBox',`${x0} ${y0} ${hw*2} ${hh*2}`) } } diff --git a/_datafiles/html/public/webclient-pure.html b/_datafiles/html/public/webclient-pure.html index c2b39f34..7dbed51d 100644 --- a/_datafiles/html/public/webclient-pure.html +++ b/_datafiles/html/public/webclient-pure.html @@ -250,7 +250,10 @@

Volume Controls

///////////////////////////////////////////// let GMCPStructs = {}; let gr = {}; - let allRooms = {}; + let allRooms = { + currentZoneKey: "", + roomZones: {}, + }; let GMCPUpdateHandlers = { "Room": function() { @@ -261,53 +264,110 @@

Volume Controls

if ( !GMCPWindows['Map'] && GMCPWindows['Map'] !== false ) { - - GMCPWindows['Map'] = new WinBox({ title: "Map", - mount: document.getElementById("map-render"), - background: "#1c6b60", - border: 1, - width:600, - height:20+600, - header: 20, - onclose: force => { GMCPWindows['Map'] = false; return false; }, - oncreate: ops => { - gr = new GraphRenderer({ - container: document.getElementById('map-render'), - onRoomClick: (graph, id) => { - graph.centerOn(id); - } - }); - }, + mount: document.getElementById("map-render"), + background: "#1c6b60", + border: 1, + x: "right", + y: 0, + width:300, + height:20+300, + header: 20, + onclose: force => { GMCPWindows['Map'] = false; return false; }, + oncreate: ops => { + + var mapOptions = { cellSize: 80, + cellMargin: 80, + initialZoom: 0.5 }; + //mapOptions.onRoomClick = function(r) { alert(`Room ${r.RoomId} clicked`); } + + gr = new RoomGridSVG('#map-render', mapOptions); + + + }, }); } - if ( GMCPWindows['Map'] === false ) { return; } - coords = obj.Info.coords.split(","); - var r = { RoomId: obj.Info.num, x: parseInt(coords[1]), y: parseInt(coords[2]), Exits: {} } - for (var p in obj.Info.exits ) { - r.Exits[p] = { RoomId: obj.Info.exits[p].num }; - } + if ( obj.Info ) { + + GMCPWindows['Map'].setTitle('Map ('+obj.Info.area+')'); + var coords = obj.Info.coords.split(","); + for( var i=0; iVolume Controls mount: document.getElementById("vitals-bars"), background: "#1c6b60", border: 1, + x: "right", + y: 320, width:300, height:20+120, header: 20, @@ -996,9 +1058,9 @@

Volume Controls

#vitals-bars { height:100%; } - .wb-body { - background:#000; - } + .wb-body { background:#000; } + .wb-max { display:none; } + .wb-full { display:none; }
From 8ab173349b18baf1a04e57f07228d02161f61ee1 Mon Sep 17 00:00:00 2001 From: Volte6 <143822+Volte6@users.noreply.github.com> Date: Tue, 6 May 2025 14:08:48 -0700 Subject: [PATCH 6/6] formatting fixes --- _datafiles/html/public/static/js/gmcp.js | 531 ++++++++++++----------- 1 file changed, 276 insertions(+), 255 deletions(-) diff --git a/_datafiles/html/public/static/js/gmcp.js b/_datafiles/html/public/static/js/gmcp.js index c61f6e15..1b48a632 100644 --- a/_datafiles/html/public/static/js/gmcp.js +++ b/_datafiles/html/public/static/js/gmcp.js @@ -1,44 +1,44 @@ class RoomGridSVG { constructor(selector, options = {}) { - // ── Configurable options & defaults ─────────────────────────────── - this.cellSize = options.cellSize || 100 - this.cellMargin = options.cellMargin || 20 - this.spacing = this.cellSize + this.cellMargin - this.zoomStep = options.zoomStep || 1.2 - this.zoomLevel = options.initialZoom || 1 - this.onRoomClick = options.onRoomClick || (() => {}) - this.zoomButtonSize = options.zoomButtonSize || 25 - this.controlsMargin = options.controlsMargin || 10 - this.roomEdgeColor = options.roomEdgeColor || "#1c6b60" - this.visitingColor = options.visitingColor || "#c20000" - // ── Internal state ──────────────────────────────────────────────── - // rooms: Map - this.rooms = new Map() - this.drawnEdges = new Set() // to avoid dup lines - this.currentCenterId = null // for highlight - - // ── Build container & SVG ───────────────────────────────────────── - this.container = document.querySelector(selector) - this.container.style.position = 'relative' - - this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg') - this.svg.setAttribute('preserveAspectRatio','xMidYMid meet') - this.svg.style.width = '100%' - this.svg.style.height = '100%' - this.container.appendChild(this.svg) - - // Connections under rooms: - this.connectionsGroup = document.createElementNS(this.svg.namespaceURI,'g') - this.svg.appendChild(this.connectionsGroup) - // Rooms on top: - this.roomsGroup = document.createElementNS(this.svg.namespaceURI,'g') - this.svg.appendChild(this.roomsGroup) - - // Default tiny viewBox until rooms exist: - this.svg.setAttribute('viewBox','0 0 1 1') - - // ── HTML overlay zoom controls ──────────────────────────────────── - this._createHTMLControls() + // ── Configurable options & defaults ─────────────────────────────── + this.cellSize = options.cellSize || 100; + this.cellMargin = options.cellMargin || 20; + this.spacing = this.cellSize + this.cellMargin; + this.zoomStep = options.zoomStep || 1.2; + this.zoomLevel = options.initialZoom || 1; + this.onRoomClick = options.onRoomClick || (() => {}); + this.zoomButtonSize = options.zoomButtonSize || 25; + this.controlsMargin = options.controlsMargin || 10; + this.roomEdgeColor = options.roomEdgeColor || "#1c6b60"; + this.visitingColor = options.visitingColor || "#c20000"; + // ── Internal state ──────────────────────────────────────────────── + // rooms: Map + this.rooms = new Map(); + this.drawnEdges = new Set(); // to avoid dup lines + this.currentCenterId = null; // for highlight + + // ── Build container & SVG ───────────────────────────────────────── + this.container = document.querySelector(selector); + this.container.style.position = 'relative'; + + this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); + this.svg.style.width = '100%'; + this.svg.style.height = '100%'; + this.container.appendChild(this.svg); + + // Connections under rooms: + this.connectionsGroup = document.createElementNS(this.svg.namespaceURI, 'g'); + this.svg.appendChild(this.connectionsGroup); + // Rooms on top: + this.roomsGroup = document.createElementNS(this.svg.namespaceURI, 'g'); + this.svg.appendChild(this.roomsGroup); + + // Default tiny viewBox until rooms exist: + this.svg.setAttribute('viewBox', '0 0 1 1'); + + // ── HTML overlay zoom controls ──────────────────────────────────── + this._createHTMLControls(); } // ── Public API ─────────────────────────────────────────────────────── @@ -49,268 +49,289 @@ class RoomGridSVG { * - If room already exists, updates its position, color, text, & redraws edges. */ addRoom(room) { - const id = room.RoomId - - // 1) Pre-add exit-defined rooms - if (Array.isArray(room.Exits)) { - room.Exits.forEach(e => { - if (e && typeof e==='object' && e.RoomId != null) { - - if (this.rooms.has(e.RoomId)) return; - - this.addRoom({ - RoomId: e.RoomId, - Text: e.Text != null?e.Text:String(e.RoomId), - x: e.x, y: e.y, - Exits: Array.isArray(e.Exits)?e.Exits:[] - }) - } - }) - } - - // prepare defaults - const defaultColor = room.Color || '#fff' - const displayText = room.Text != null - ? room.Text - : String(room.RoomId) - - // 2) UPDATE existing - if (this.rooms.has(id)) { - const entry = this.rooms.get(id) - // update stored data - entry.room.x = room.x - entry.room.y = room.y - entry.room.Exits = Array.isArray(room.Exits)?room.Exits:[] - entry.room.Color = room.Color - entry.room.Text = room.Text - entry.defaultColor = defaultColor - - // move & recolor rect - const rect = this.svg.querySelector(`rect[data-room-rect="${id}"]`) - rect.setAttribute('x', room.x * this.spacing) - rect.setAttribute('y', room.y * this.spacing) - if (this.currentCenterId === id) { - rect.setAttribute('fill', this.visitingColor) - } else { - rect.setAttribute('fill', defaultColor) + const id = room.RoomId; + + // 1) Pre-add exit-defined rooms + if (Array.isArray(room.Exits)) { + room.Exits.forEach(e => { + if (e && typeof e === 'object' && e.RoomId != null) { + + if (this.rooms.has(e.RoomId)) return; + + this.addRoom({ + RoomId: e.RoomId, + Text: e.Text != null ? e.Text : String(e.RoomId), + x: e.x, + y: e.y, + Exits: Array.isArray(e.Exits) ? e.Exits : [] + }); + } + }); } - // move & update label - const txtEl = this.svg.querySelector(`g[data-room-id="${id}"] text`) - txtEl.setAttribute('x', room.x * this.spacing + this.cellSize/2) - txtEl.setAttribute('y', room.y * this.spacing + this.cellSize/2 + 5) - txtEl.textContent = displayText + // prepare defaults + const defaultColor = room.Color || '#fff'; + const displayText = room.Text != null ? + room.Text : + String(room.RoomId); + + // 2) UPDATE existing + if (this.rooms.has(id)) { + const entry = this.rooms.get(id); + // update stored data + entry.room.x = room.x; + entry.room.y = room.y; + entry.room.Exits = Array.isArray(room.Exits) ? room.Exits : []; + entry.room.Color = room.Color; + entry.room.Text = room.Text; + entry.defaultColor = defaultColor; + + // move & recolor rect + const rect = this.svg.querySelector(`rect[data-room-rect="${id}"]`); + rect.setAttribute('x', room.x * this.spacing); + rect.setAttribute('y', room.y * this.spacing); + if (this.currentCenterId === id) { + rect.setAttribute('fill', this.visitingColor); + } else { + rect.setAttribute('fill', defaultColor); + } + + // move & update label + const txtEl = this.svg.querySelector(`g[data-room-id="${id}"] text`); + txtEl.setAttribute('x', room.x * this.spacing + this.cellSize / 2); + txtEl.setAttribute('y', room.y * this.spacing + this.cellSize / 2 + 5); + txtEl.textContent = displayText; + + // redraw any new edges + this._drawEdgesForRoom(id); + + // refresh bounds & view + this._updateBounds(); + this._applyZoom(); + return; + } - // redraw any new edges - this._drawEdgesForRoom(id) + // 3) NEW room → draw group + const g = document.createElementNS(this.svg.namespaceURI, 'g'); + g.setAttribute('data-room-id', id); + + // square + const rect = document.createElementNS(this.svg.namespaceURI, 'rect'); + rect.setAttribute('width', this.cellSize); + rect.setAttribute('height', this.cellSize); + rect.setAttribute('x', room.x * this.spacing); + rect.setAttribute('y', room.y * this.spacing); + rect.setAttribute('stroke', this.roomEdgeColor); + rect.setAttribute('stroke-width', '4'); + rect.setAttribute('rx', this.cellSize / 10); // corner radius X + rect.setAttribute('ry', this.cellSize / 10); // corner radius Y + rect.setAttribute('data-room-rect', id); + rect.setAttribute('fill', defaultColor); + rect.style.cursor = 'pointer'; + rect.addEventListener('click', () => this.onRoomClick(room)); + g.appendChild(rect); + + // label + const label = document.createElementNS(this.svg.namespaceURI, 'text'); + label.setAttribute('x', room.x * this.spacing + this.cellSize / 2); + label.setAttribute('y', room.y * this.spacing + this.cellSize / 2 + 5); + label.setAttribute('text-anchor', 'middle'); + label.setAttribute('font-size', this.cellSize * 0.3); + label.textContent = displayText; + g.appendChild(label); + + this.roomsGroup.appendChild(g); + this.rooms.set(id, { + room, + group: g, + defaultColor + }); + + // draw edges for this new room + this._drawEdgesForRoom(id); // refresh bounds & view - this._updateBounds() - this._applyZoom() - return - } - - // 3) NEW room → draw group - const g = document.createElementNS(this.svg.namespaceURI,'g') - g.setAttribute('data-room-id', id) - - // square - const rect = document.createElementNS(this.svg.namespaceURI,'rect') - rect.setAttribute('width', this.cellSize) - rect.setAttribute('height', this.cellSize) - rect.setAttribute('x', room.x * this.spacing) - rect.setAttribute('y', room.y * this.spacing) - rect.setAttribute('stroke', this.roomEdgeColor) - rect.setAttribute('stroke-width','4') - rect.setAttribute('rx', this.cellSize/10); // corner radius X - rect.setAttribute('ry', this.cellSize/10); // corner radius Y - rect.setAttribute('data-room-rect', id) - rect.setAttribute('fill', defaultColor) - rect.style.cursor = 'pointer' - rect.addEventListener('click', () => this.onRoomClick(room)) - g.appendChild(rect) - - // label - const label = document.createElementNS(this.svg.namespaceURI,'text') - label.setAttribute('x', room.x * this.spacing + this.cellSize/2) - label.setAttribute('y', room.y * this.spacing + this.cellSize/2 + 5) - label.setAttribute('text-anchor', 'middle') - label.setAttribute('font-size', this.cellSize * 0.3) - label.textContent = displayText - g.appendChild(label) - - this.roomsGroup.appendChild(g) - this.rooms.set(id, { room, group: g, defaultColor }) - - // draw edges for this new room - this._drawEdgesForRoom(id) - - // refresh bounds & view - this._updateBounds() - this._applyZoom() + this._updateBounds(); + this._applyZoom(); } /** * Bulk‐set rooms (wipes existing). */ setRooms(arr) { - this.reset() - arr.forEach(r => this.addRoom(r)) + this.reset(); + arr.forEach(r => this.addRoom(r)); } /** * Clear everything. */ reset() { - this.rooms.clear() - this.drawnEdges.clear() - this.currentCenterId = null - this.zoomLevel = 1 - this.svg.setAttribute('viewBox','0 0 1 1') - this.roomsGroup.innerHTML = '' - this.connectionsGroup.innerHTML = '' + this.rooms.clear(); + this.drawnEdges.clear(); + this.currentCenterId = null; + this.zoomLevel = 1; + this.svg.setAttribute('viewBox', '0 0 1 1'); + this.roomsGroup.innerHTML = ''; + this.connectionsGroup.innerHTML = ''; } /** * Center & highlight a room. Previous one reverts to its default color. */ centerOnRoom(id) { - const entry = this.rooms.get(id) - if (!entry) return - - // un-highlight previous - if (this.currentCenterId != null) { - const prevRect = this.svg.querySelector( - `rect[data-room-rect="${this.currentCenterId}"]` - ) - if (prevRect) { - const prevEntry = this.rooms.get(this.currentCenterId) - prevRect.setAttribute('fill', prevEntry.defaultColor) + const entry = this.rooms.get(id); + if (!entry) return; + + // un-highlight previous + if (this.currentCenterId != null) { + const prevRect = this.svg.querySelector( + `rect[data-room-rect="${this.currentCenterId}"]` + ); + if (prevRect) { + const prevEntry = this.rooms.get(this.currentCenterId); + prevRect.setAttribute('fill', prevEntry.defaultColor); + } } - } - - // compute new view center - this.center = { - x: entry.room.x * this.spacing + this.cellSize/2, - y: entry.room.y * this.spacing + this.cellSize/2 - } - this._applyZoom() - - // highlight new - const newRect = this.svg.querySelector( - `rect[data-room-rect="${id}"]` - ) - if (newRect) newRect.setAttribute('fill', this.visitingColor) - - this.currentCenterId = id + + // compute new view center + this.center = { + x: entry.room.x * this.spacing + this.cellSize / 2, + y: entry.room.y * this.spacing + this.cellSize / 2 + }; + this._applyZoom(); + + // highlight new + const newRect = this.svg.querySelector( + `rect[data-room-rect="${id}"]` + ); + if (newRect) newRect.setAttribute('fill', this.visitingColor); + + this.currentCenterId = id; } - zoomIn() { this.zoomLevel *= this.zoomStep; this._applyZoom() } - zoomOut() { this.zoomLevel /= this.zoomStep; this._applyZoom() } + zoomIn() { + this.zoomLevel *= this.zoomStep; + this._applyZoom(); + } + zoomOut() { + this.zoomLevel /= this.zoomStep; + this._applyZoom(); + } drawConnection(a, b) { - if (!this.rooms.has(a) || !this.rooms.has(b)) return - this._drawEdge(a, b) - this._applyZoom() + if (!this.rooms.has(a) || !this.rooms.has(b)) return; + this._drawEdge(a, b); + this._applyZoom(); } // ── Private draw helpers ─────────────────────────────────────────────── _createHTMLControls() { - const div = document.createElement('div') - div.style.cssText = ` - position:absolute; - top:${this.controlsMargin}px; - right:${this.controlsMargin}px; - display:flex; gap:5px; - ` - const mk = (lbl, cb) => { - const b = document.createElement('button') - b.textContent = lbl - b.style.cssText = ` - width:${this.zoomButtonSize}px; - height:${this.zoomButtonSize}px; - font-size:${this.zoomButtonSize*0.6}px; - line-height:1; - ` - b.addEventListener('click', cb) - return b - } - div.append(mk('−',()=>this.zoomOut()), mk('+',()=>this.zoomIn())) - this.container.appendChild(div) + const div = document.createElement('div'); + div.style.cssText = ` + position:absolute; + top:${this.controlsMargin}px; + right:${this.controlsMargin}px; + display:flex; gap:5px; + `; + const mk = (lbl, cb) => { + const b = document.createElement('button'); + b.textContent = lbl; + b.style.cssText = ` + width:${this.zoomButtonSize}px; + height:${this.zoomButtonSize}px; + font-size:${this.zoomButtonSize*0.6}px; + line-height:1; + `; + b.addEventListener('click', cb); + return b; + }; + div.append(mk('−', () => this.zoomOut()), mk('+', () => this.zoomIn())); + this.container.appendChild(div); } _drawEdgesForRoom(id) { - const me = this.rooms.get(id).room - const exits = Array.isArray(me.Exits)?me.Exits:[] - - // draw its own exits - exits.forEach(e => { - const to = (typeof e==='object')?e.RoomId:e - if (this.rooms.has(to)) this._drawEdge(id, to) - }) - - // draw others’ exits back to it - this.rooms.forEach(({ room }, otherId) => { - if (otherId===id) return - const oe = Array.isArray(room.Exits)?room.Exits:[] - if (oe.some(x => ((typeof x==='object')?x.RoomId:x)===id)) { - this._drawEdge(otherId, id) - } - }) + const me = this.rooms.get(id) + .room; + const exits = Array.isArray(me.Exits) ? me.Exits : []; + + // draw its own exits + exits.forEach(e => { + const to = (typeof e === 'object') ? e.RoomId : e; + if (this.rooms.has(to)) this._drawEdge(id, to); + }); + + // draw others’ exits back to it + this.rooms.forEach(({ + room + }, otherId) => { + if (otherId === id) return; + const oe = Array.isArray(room.Exits) ? room.Exits : []; + if (oe.some(x => ((typeof x === 'object') ? x.RoomId : x) === id)) { + this._drawEdge(otherId, id); + } + }); } _drawEdge(a, b) { - const key = ae.room.x) - const ys = [...this.rooms.values()].map(e=>e.room.y) - this.bounds = { - minX: Math.min(...xs), - maxX: Math.max(...xs), - minY: Math.min(...ys), - maxY: Math.max(...ys) + if (!this.rooms.size) { + this.bounds = { + minX: 0, + maxX: 0, + minY: 0, + maxY: 0 + }; + } else { + const xs = [...this.rooms.values()].map(e => e.room.x); + const ys = [...this.rooms.values()].map(e => e.room.y); + this.bounds = { + minX: Math.min(...xs), + maxX: Math.max(...xs), + minY: Math.min(...ys), + maxY: Math.max(...ys) + }; } - } - this.worldWidth = (this.bounds.maxX - this.bounds.minX + 1)*this.spacing - this.worldHeight = (this.bounds.maxY - this.bounds.minY + 1)*this.spacing - - if (!this.center && this.rooms.size) { - this.center = { - x: this.bounds.minX*this.spacing + this.worldWidth/2, - y: this.bounds.minY*this.spacing + this.worldHeight/2 + this.worldWidth = (this.bounds.maxX - this.bounds.minX + 1) * this.spacing; + this.worldHeight = (this.bounds.maxY - this.bounds.minY + 1) * this.spacing; + + if (!this.center && this.rooms.size) { + this.center = { + x: this.bounds.minX * this.spacing + this.worldWidth / 2, + y: this.bounds.minY * this.spacing + this.worldHeight / 2 + }; } - } } _applyZoom() { - const hw = this.worldWidth / (2*this.zoomLevel) - const hh = this.worldHeight / (2*this.zoomLevel) - const x0 = (this.center?.x||this.worldWidth/2) - hw - const y0 = (this.center?.y||this.worldHeight/2) - hh - this.svg.setAttribute('viewBox',`${x0} ${y0} ${hw*2} ${hh*2}`) + const hw = this.worldWidth / (2 * this.zoomLevel); + const hh = this.worldHeight / (2 * this.zoomLevel); + const x0 = (this.center ? this.center.x : this.worldWidth / 2) - hw; + const y0 = (this.center ? this.center.y : this.worldHeight / 2) - hh; + this.svg.setAttribute('viewBox', `${x0} ${y0} ${hw*2} ${hh*2}`); } }