BlueMap addon, shop points, map actions wired into Paper and Fabric; client opens shop windows
No files matched your search
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#22d3ee" stroke="#ffffff" stroke-width="2"/><path d="M16 6l8 3v6c0 5-3.4 8.6-8 10-4.6-1.4-8-5-8-10V9z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 3l11 4v8c0 7-4.6 12-11 14C9.6 27 5 22 5 15V7z" fill="#8b6cff" stroke="#fff" stroke-width="2"/><path d="M16 9l2.2 4.6 5 .7-3.6 3.5.9 5L16 20.4 11.5 22.8l.9-5-3.6-3.5 5-.7z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 288 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#fbbf24" stroke="#ffffff" stroke-width="2"/><path d="M16 7l9 8h-3v8h-4v-5h-4v5h-4v-8H7z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#fb7185" stroke="#ffffff" stroke-width="2"/><path d="M7 12l2-5h14l2 5v2c0 1.7-1.3 3-3 3s-3-1.3-3-3c0 1.7-1.3 3-3 3s-3-1.3-3-3c0 1.7-1.3 3-3 3s-3-1.3-3-3zM9 19h14v6H9z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,64 @@
|
||||
/* SCOPENET on BlueMap: marker styling and the player side panel. Loaded by BlueMap through the addon. */
|
||||
|
||||
/* Guild icons sit on the map as small rounded badges, whatever size the uploaded image is. */
|
||||
.scopenet-guild-icon img,
|
||||
.bm-marker-poi.scopenet-guild-icon img {
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
object-fit: cover;
|
||||
border-radius: 9px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
|
||||
background: #1a1d25;
|
||||
}
|
||||
.scopenet-pin img,
|
||||
.bm-marker-poi.scopenet-pin img {
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.55));
|
||||
}
|
||||
|
||||
/* Popup text inside marker details. */
|
||||
.scopenet-detail { font-size: 13px; line-height: 1.45; min-width: 160px; }
|
||||
.scopenet-detail span { opacity: 0.7; }
|
||||
.scopenet-detail code { background: rgba(255, 255, 255, 0.08); padding: 1px 5px; border-radius: 4px; }
|
||||
.scopenet-list { margin: 4px 0 0; padding-left: 16px; }
|
||||
|
||||
/* Player panel (shown in a plain browser; inside the SCOPENET launcher the launcher shows its own). */
|
||||
#scopenet-panel {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 14px;
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 90px);
|
||||
overflow-y: auto;
|
||||
z-index: 50;
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(20, 22, 30, 0.96);
|
||||
color: #e6e8ee;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.75);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
animation: scopenet-in 0.18s ease;
|
||||
}
|
||||
@keyframes scopenet-in { from { opacity: 0; transform: translateX(18px); } }
|
||||
#scopenet-panel button.close {
|
||||
position: absolute; top: 8px; right: 8px; width: 26px; height: 26px; border: 0; border-radius: 8px;
|
||||
background: transparent; color: #9aa0ae; font-size: 18px; cursor: pointer;
|
||||
}
|
||||
#scopenet-panel button.close:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
|
||||
#scopenet-panel .head { display: flex; gap: 12px; align-items: center; padding-right: 26px; }
|
||||
#scopenet-panel .head img { width: 56px; height: 56px; border-radius: 16px; border: 3px solid #8b6cff; background: #1a1d25; image-rendering: pixelated; }
|
||||
#scopenet-panel h3 { margin: 0 0 2px; font-size: 17px; }
|
||||
#scopenet-panel .title { color: #fbbf24; font-size: 12px; }
|
||||
#scopenet-panel .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
|
||||
#scopenet-panel .chip { padding: 2px 9px; border-radius: 99px; background: rgba(255, 255, 255, 0.08); font-size: 11px; font-weight: 650; }
|
||||
#scopenet-panel .chip.lv { color: #8b6cff; }
|
||||
#scopenet-panel .chip.guild { color: #22d3ee; }
|
||||
#scopenet-panel .chip.rank { color: #fbbf24; }
|
||||
#scopenet-panel .facts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
#scopenet-panel .facts div { padding: 8px 10px; border-radius: 10px; background: rgba(255, 255, 255, 0.05); color: #9aa0ae; font-size: 11px; }
|
||||
#scopenet-panel .facts strong { display: block; color: #e6e8ee; font-size: 14px; margin-top: 2px; }
|
||||
#scopenet-panel .note { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px; background: rgba(139, 108, 255, 0.12); color: #c9bfff; font-size: 12px; line-height: 1.4; }
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SCOPENET on BlueMap. Registered by the SCOPENET addon, so it runs inside BlueMap's own page: the same in a browser and
|
||||
* inside the SCOPENET launcher.
|
||||
*
|
||||
* - Click a player on the map to see who they are (level, guild, rank, title...), read from players.json that the
|
||||
* server keeps up to date next to this file.
|
||||
* - Inside the launcher (this page is in an iframe) the click is handed to the launcher, which shows the full side panel
|
||||
* with the actions that need a signed-in account: teleport request, message, guild invite.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var BASE = 'assets/scopenet/';
|
||||
var PLAYER_PREFIX = 'bm-marker-bm-player-';
|
||||
var embedded = window.parent && window.parent !== window;
|
||||
var players = {};
|
||||
var selected = null;
|
||||
|
||||
function esc(text) {
|
||||
return String(text == null ? '' : text).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
/** The uuid behind a player marker element, or null. BlueMap ids them "bm-marker-bm-player-<uuid>". */
|
||||
function uuidFromId(id) {
|
||||
return typeof id === 'string' && id.indexOf(PLAYER_PREFIX) === 0 ? id.substring(PLAYER_PREFIX.length) : null;
|
||||
}
|
||||
|
||||
/** Which player marker is under this screen point? Works even where the marker ignores pointer events. */
|
||||
function playerAt(x, y) {
|
||||
var nodes = document.querySelectorAll('[id^="' + PLAYER_PREFIX + '"]');
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var r = nodes[i].getBoundingClientRect();
|
||||
if (r.width === 0 && r.height === 0) continue;
|
||||
if (x >= r.left - 6 && x <= r.right + 6 && y >= r.top - 6 && y <= r.bottom + 6) return uuidFromId(nodes[i].id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hours(seconds) {
|
||||
seconds = Number(seconds) || 0;
|
||||
return seconds >= 3600 ? Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm' : Math.floor(seconds / 60) + 'm';
|
||||
}
|
||||
|
||||
function panel() {
|
||||
var el = document.getElementById('scopenet-panel');
|
||||
if (!el) { el = document.createElement('div'); el.id = 'scopenet-panel'; document.body.appendChild(el); }
|
||||
return el;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var el = document.getElementById('scopenet-panel');
|
||||
if (!selected) { if (el) el.remove(); return; }
|
||||
var p = players[selected] || players[selected.toLowerCase()] || {};
|
||||
el = panel();
|
||||
var name = p.name || 'Player';
|
||||
el.innerHTML =
|
||||
'<button class="close" aria-label="Close">×</button>' +
|
||||
'<div class="head"><img alt="" src="assets/playerheads/' + esc(selected) + '.png" onerror="this.src=\'assets/steve.png\'">' +
|
||||
'<div><h3>' + esc(name) + '</h3>' + (p.title ? '<div class="title">' + esc(p.title) + '</div>' : '') + '</div></div>' +
|
||||
'<div class="chips">' +
|
||||
'<span class="chip lv">Level ' + esc(p.level || 1) + '</span>' +
|
||||
(p.guild ? '<span class="chip guild">[' + esc(p.guildTag) + '] ' + esc(p.guild) + '</span>' : '') +
|
||||
(p.rank ? '<span class="chip rank">' + esc(p.rank) + '</span>' : '') + '</div>' +
|
||||
'<div class="facts">' +
|
||||
'<div>Playtime<strong>' + esc(hours(p.playtime)) + '</strong></div>' +
|
||||
'<div>Server level<strong>' + esc(p.serverLevel || 1) + '</strong></div>' +
|
||||
(p.guildRole ? '<div>Guild role<strong>' + esc(p.guildRole) + '</strong></div>' : '') + '</div>' +
|
||||
'<p class="note">Open this map in the SCOPENET launcher to send a teleport request or message, or to invite ' + esc(name) + ' to your guild.</p>';
|
||||
el.querySelector('.close').addEventListener('click', function () { select(null); });
|
||||
}
|
||||
|
||||
function select(uuid) {
|
||||
selected = uuid;
|
||||
if (embedded) {
|
||||
window.parent.postMessage(uuid
|
||||
? { source: 'scopenet-map', type: 'player', uuid: uuid, name: (players[uuid] || {}).name || '' }
|
||||
: { source: 'scopenet-map', type: 'deselect' }, '*');
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(BASE + 'players.json?t=' + Date.now(), { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (j) { if (j && j.players) { players = j.players; if (!embedded && selected) render(); } })
|
||||
.catch(function () { /* the file appears once the server has written it */ });
|
||||
}
|
||||
|
||||
// A click is a press and release in nearly the same place; a drag moves the map.
|
||||
var down = null;
|
||||
document.addEventListener('pointerdown', function (e) { down = { x: e.clientX, y: e.clientY, t: Date.now() }; }, true);
|
||||
document.addEventListener('pointerup', function (e) {
|
||||
var start = down;
|
||||
down = null;
|
||||
if (!start || e.button > 0) return;
|
||||
if (Math.hypot(e.clientX - start.x, e.clientY - start.y) > 6 || Date.now() - start.t > 700) return;
|
||||
if (e.target && e.target.closest && e.target.closest('#scopenet-panel')) return;
|
||||
var uuid = playerAt(e.clientX, e.clientY);
|
||||
if (uuid) select(uuid);
|
||||
else if (selected) select(null);
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (e) {
|
||||
if (e.source === window.parent && e.data && e.data.source === 'scopenet-launcher' && e.data.type === 'hello') { /* acknowledged */ }
|
||||
});
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 10000);
|
||||
if (embedded) window.parent.postMessage({ source: 'scopenet-map', type: 'ready' }, '*');
|
||||
|
||||
// Exposed for tests.
|
||||
window.__scopenet = { uuidFromId: uuidFromId, esc: esc, hours: hours };
|
||||
})();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#f97316" stroke="#ffffff" stroke-width="2"/><path d="M8 10h3l2 10h10l2-7H12" fill="none" stroke="#fff" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/><circle cx="14" cy="24" r="1.8" fill="#fff"/><circle cx="22" cy="24" r="1.8" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#34d399" stroke="#ffffff" stroke-width="2"/><path d="M16 7l2.6 6.4 6.9.5-5.3 4.5 1.7 6.7L16 21.3 10.1 25l1.7-6.7-5.3-4.5 6.9-.5z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#8b6cff" stroke="#ffffff" stroke-width="2"/><circle cx="16" cy="16" r="6" fill="none" stroke="#fff" stroke-width="2.5"/><circle cx="16" cy="16" r="2" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 290 B |