/* ---------- DOM references --------------------------------------- */ const wrap = document.getElementById('mapContainer'); const group = document.getElementById('mapGroup'); const img = document.getElementById('map'); const dots = document.getElementById('dots'); const list = document.getElementById('playerList'); const btnContainer = document.getElementById('sortButtons'); const tooltip = document.getElementById('tooltip'); /* ---------- constants ------------------------------------------- */ const MAX_Z = 10; const FOCUS_ZOOM = 3; // zoom level when you click a name const POLL_MS = 2000; const MAP_BOUNDS = { west : -102.04, east : 102.19, north: 102.16, south: -102.00 }; /* ---------- sort configuration ---------------------------------- */ const sortOptions = [ { value: "name", label: "Name ↑", comparator: (a, b) => a.character_name.localeCompare(b.character_name) }, { value: "kills", label: "Kills ↓", comparator: (a, b) => b.kills - a.kills }, { value: "rares", label: "Rares ↓", comparator: (a, b) => (b.rares_found || 0) - (a.rares_found || 0) } ]; let currentSort = sortOptions[0]; let currentPlayers = []; /* ---------- generate segmented buttons -------------------------- */ sortOptions.forEach(opt => { const btn = document.createElement('div'); btn.className = 'btn'; btn.textContent = opt.label; btn.dataset.value = opt.value; if (opt.value === currentSort.value) btn.classList.add('active'); btn.addEventListener('click', () => { btnContainer.querySelectorAll('.btn') .forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentSort = opt; renderList(); }); btnContainer.appendChild(btn); }); /* ---------- map & state variables ------------------------------- */ let imgW = 0, imgH = 0; let scale = 1, offX = 0, offY = 0, minScale = 1; let dragging = false, sx = 0, sy = 0; let selected = ""; let pollID = null; /* ---------- utility functions ----------------------------------- */ const hue = name => { let h = 0; for (let c of name) h = c.charCodeAt(0) + ((h << 5) - h); return `hsl(${Math.abs(h) % 360},72%,50%)`; }; const loc = (ns, ew) => `${Math.abs(ns).toFixed(1)}${ns>=0?"N":"S"} ` + `${Math.abs(ew).toFixed(1)}${ew>=0?"E":"W"}`; function worldToPx(ew, ns) { const x = ((ew - MAP_BOUNDS.west) / (MAP_BOUNDS.east - MAP_BOUNDS.west)) * imgW; const y = ((MAP_BOUNDS.north - ns) / (MAP_BOUNDS.north - MAP_BOUNDS.south)) * imgH; return { x, y }; } const applyTransform = () => group.style.transform = `translate(${offX}px,${offY}px) scale(${scale})`; function clampPan() { if (!imgW) return; const r = wrap.getBoundingClientRect(); const vw = r.width, vh = r.height; const mw = imgW * scale, mh = imgH * scale; offX = mw <= vw ? (vw - mw) / 2 : Math.min(0, Math.max(vw - mw, offX)); offY = mh <= vh ? (vh - mh) / 2 : Math.min(0, Math.max(vh - mh, offY)); } function updateView() { clampPan(); applyTransform(); } function fitToWindow() { const r = wrap.getBoundingClientRect(); scale = Math.min(r.width / imgW, r.height / imgH); minScale = scale; updateView(); } /* ---------- tooltip handlers ------------------------------------ */ function showTooltip(evt, p) { tooltip.textContent = `${p.character_name} — Kills: ${p.kills} - Kills/h: ${p.kills_per_hour}`; const r = wrap.getBoundingClientRect(); tooltip.style.left = `${evt.clientX - r.left + 10}px`; tooltip.style.top = `${evt.clientY - r.top + 10}px`; tooltip.style.display = 'block'; } function hideTooltip() { tooltip.style.display = 'none'; } /* ---------- polling and initialization -------------------------- */ async function pollLive() { try { const { players } = await (await fetch('/live/')).json(); currentPlayers = players; renderList(); } catch (e) { console.error('Live fetch failed:', e); } } function startPolling() { if (pollID !== null) return; pollLive(); pollID = setInterval(pollLive, POLL_MS); } img.onload = () => { imgW = img.naturalWidth; imgH = img.naturalHeight; fitToWindow(); startPolling(); }; /* ---------- rendering sorted list & dots ------------------------ */ function renderList() { const sorted = [...currentPlayers].sort(currentSort.comparator); render(sorted); } function render(players) { dots.innerHTML = ''; list.innerHTML = ''; players.forEach(p => { const { x, y } = worldToPx(p.ew, p.ns); // dot const dot = document.createElement('div'); dot.className = 'dot'; dot.style.left = `${x}px`; dot.style.top = `${y}px`; dot.style.background = hue(p.character_name); // custom tooltip dot.addEventListener('mouseenter', e => showTooltip(e, p)); dot.addEventListener('mousemove', e => showTooltip(e, p)); dot.addEventListener('mouseleave', hideTooltip); // click to select/zoom dot.addEventListener('click', () => selectPlayer(p, x, y)); if (p.character_name === selected) dot.classList.add('highlight'); dots.appendChild(dot); //sidebar const li = document.createElement('li'); const color = hue(p.character_name); li.style.borderLeftColor = color; li.className = 'player-item'; li.innerHTML = ` ${p.character_name} ${loc(p.ns, p.ew)} ${p.kills} ${p.kills_per_hour} ${p.rares_found} ${p.vt_state} `; li.addEventListener('click', () => selectPlayer(p, x, y)); if (p.character_name === selected) li.classList.add('selected'); list.appendChild(li); }); } /* ---------- selection centering, focus zoom & blink ------------ */ function selectPlayer(p, x, y) { selected = p.character_name; // set focus zoom scale = Math.min(MAX_Z, Math.max(minScale, FOCUS_ZOOM)); // center on the player const r = wrap.getBoundingClientRect(); offX = r.width / 2 - x * scale; offY = r.height / 2 - y * scale; updateView(); renderList(); // keep sorted + highlight } /* ---------- pan & zoom handlers -------------------------------- */ wrap.addEventListener('wheel', e => { e.preventDefault(); if (!imgW) return; const r = wrap.getBoundingClientRect(); const mx = (e.clientX - r.left - offX) / scale; const my = (e.clientY - r.top - offY) / scale; const factor = e.deltaY > 0 ? 0.9 : 1.1; let ns = scale * factor; ns = Math.max(minScale, Math.min(MAX_Z, ns)); offX -= mx * (ns - scale); offY -= my * (ns - scale); scale = ns; updateView(); }, { passive: false }); wrap.addEventListener('mousedown', e => { dragging = true; sx = e.clientX; sy = e.clientY; wrap.classList.add('dragging'); }); window.addEventListener('mousemove', e => { if (!dragging) return; offX += e.clientX - sx; offY += e.clientY - sy; sx = e.clientX; sy = e.clientY; updateView(); }); window.addEventListener('mouseup', () => { dragging = false; wrap.classList.remove('dragging'); }); wrap.addEventListener('touchstart', e => { if (e.touches.length !== 1) return; dragging = true; sx = e.touches[0].clientX; sy = e.touches[0].clientY; }); wrap.addEventListener('touchmove', e => { if (!dragging || e.touches.length !== 1) return; const t = e.touches[0]; offX += t.clientX - sx; offY += t.clientY - sy; sx = t.clientX; sy = t.clientY; updateView(); }); wrap.addEventListener('touchend', () => { dragging = false; });