let ws = null; let roomId = null; let playerName = null; let isEnabled = false; let autoMark = false; let isDragging = false; let dragOffset = { x: 0, y: 0 }; // camera state tracking let cameraState = { scale: 1.8, offsetX: -350, offsetY: -200, gameWidth: 0, gameHeight: 0, canvasRect: null }; // spawn positions (tile coordinates) let spawnPositions = new Map(); // playerName -> {x, y} // game state let gameStarted = false; let spawnPhaseCheckInterval = null; // overlay canvas for highlights let overlayCanvas = null; let overlayCtx = null; function createOverlay() { const overlay = document.createElement('div'); overlay.id = 'spawn-tracker-overlay'; overlay.innerHTML = `
spawn tracker
disconnected
`; document.body.appendChild(overlay); chrome.storage.local.get(['overlayPosition'], (data) => { if (data.overlayPosition) { overlay.style.top = data.overlayPosition.top; overlay.style.left = data.overlayPosition.left; overlay.style.right = 'auto'; } }); const header = document.getElementById('tracker-header'); header.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); document.getElementById('tracker-toggle').addEventListener('click', toggleOverlay); document.getElementById('auto-mark-toggle').addEventListener('change', toggleAutoMark); document.getElementById('tracker-clear').addEventListener('click', clearSpawn); chrome.storage.local.get(['autoMark'], (data) => { autoMark = data.autoMark || false; document.getElementById('auto-mark-toggle').checked = autoMark; if (autoMark) { setupAutoMark(); } }); initializeHighlighting(); startSpawnPhaseCheck(); } function initializeHighlighting() { const gameCanvas = document.querySelector('canvas'); if (!gameCanvas) { setTimeout(initializeHighlighting, 500); return; } overlayCanvas = document.createElement('canvas'); overlayCanvas.id = 'spawn-highlight-overlay'; overlayCanvas.style.position = 'absolute'; overlayCanvas.style.top = '0'; overlayCanvas.style.left = '0'; overlayCanvas.style.pointerEvents = 'none'; overlayCanvas.style.zIndex = '999998'; overlayCanvas.width = window.innerWidth; overlayCanvas.height = window.innerHeight; document.body.appendChild(overlayCanvas); overlayCtx = overlayCanvas.getContext('2d'); cameraState.gameWidth = gameCanvas.width; cameraState.gameHeight = gameCanvas.height; cameraState.canvasRect = gameCanvas.getBoundingClientRect(); setupCameraTracking(gameCanvas); startHighlightLoop(); window.addEventListener('resize', () => { overlayCanvas.width = window.innerWidth; overlayCanvas.height = window.innerHeight; cameraState.canvasRect = gameCanvas.getBoundingClientRect(); }); } function setupCameraTracking(canvas) { let isPanning = false; let lastMouseX = 0; let lastMouseY = 0; let hasMoved = false; canvas.addEventListener('wheel', (e) => { const oldScale = cameraState.scale; const zoomFactor = 1 + e.deltaY / 600; cameraState.scale /= zoomFactor; cameraState.scale = Math.max(0.2, Math.min(20, cameraState.scale)); const rect = canvas.getBoundingClientRect(); const canvasX = e.clientX - rect.left; const canvasY = e.clientY - rect.top; const zoomPointX = (canvasX - cameraState.gameWidth / 2) / oldScale + cameraState.offsetX; const zoomPointY = (canvasY - cameraState.gameHeight / 2) / oldScale + cameraState.offsetY; cameraState.offsetX = zoomPointX - (canvasX - cameraState.gameWidth / 2) / cameraState.scale; cameraState.offsetY = zoomPointY - (canvasY - cameraState.gameHeight / 2) / cameraState.scale; }, { passive: true }); canvas.addEventListener('mousedown', (e) => { if (e.button === 2 || e.button === 1) { isPanning = true; hasMoved = false; lastMouseX = e.clientX; lastMouseY = e.clientY; } }); canvas.addEventListener('mousemove', (e) => { if (isPanning) { const deltaX = e.clientX - lastMouseX; const deltaY = e.clientY - lastMouseY; if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) { hasMoved = true; cameraState.offsetX -= deltaX / cameraState.scale; cameraState.offsetY -= deltaY / cameraState.scale; } lastMouseX = e.clientX; lastMouseY = e.clientY; } }); canvas.addEventListener('mouseup', () => { isPanning = false; }); document.addEventListener('mouseup', () => { isPanning = false; }); } function worldToScreenCoordinates(cellX, cellY) { const gameX = cellX; const gameY = cellY; const centerX = gameX - cameraState.gameWidth / 2; const centerY = gameY - cameraState.gameHeight / 2; const canvasX = (centerX - cameraState.offsetX) * cameraState.scale + cameraState.gameWidth / 2; const canvasY = (centerY - cameraState.offsetY) * cameraState.scale + cameraState.gameHeight / 2; const canvasRect = cameraState.canvasRect; const screenX = canvasX + canvasRect.left; const screenY = canvasY + canvasRect.top; return { x: screenX, y: screenY }; } function startHighlightLoop() { function render() { if (!overlayCtx) return; overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); spawnPositions.forEach((pos, name) => { const screen = worldToScreenCoordinates(pos.x, pos.y); const isMySpawn = name === playerName; const radius = isMySpawn ? 20 : 15; const color = isMySpawn ? '#0f0' : '#ff0'; const pulseOffset = isMySpawn ? Math.sin(Date.now() / 200) * 3 : 0; overlayCtx.beginPath(); overlayCtx.arc(screen.x, screen.y, radius + pulseOffset, 0, Math.PI * 2); overlayCtx.strokeStyle = color; overlayCtx.lineWidth = 3; overlayCtx.stroke(); overlayCtx.fillStyle = color + '33'; overlayCtx.fill(); overlayCtx.fillStyle = '#fff'; overlayCtx.font = '10px monospace'; overlayCtx.textAlign = 'center'; overlayCtx.fillText(name, screen.x, screen.y - radius - 5); }); requestAnimationFrame(render); } render(); } function startDrag(e) { isDragging = true; const overlay = document.getElementById('spawn-tracker-overlay'); const rect = overlay.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; overlay.style.cursor = 'grabbing'; } function drag(e) { if (!isDragging) return; const overlay = document.getElementById('spawn-tracker-overlay'); const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; overlay.style.left = x + 'px'; overlay.style.top = y + 'px'; overlay.style.right = 'auto'; } function stopDrag() { if (!isDragging) return; isDragging = false; const overlay = document.getElementById('spawn-tracker-overlay'); overlay.style.cursor = 'grab'; chrome.storage.local.set({ overlayPosition: { top: overlay.style.top, left: overlay.style.left } }); } function toggleOverlay() { const content = document.getElementById('tracker-content'); const btn = document.getElementById('tracker-toggle'); if (content.style.display === 'none') { content.style.display = 'block'; btn.textContent = 'hide'; } else { content.style.display = 'none'; btn.textContent = 'show'; } } function toggleAutoMark(e) { autoMark = e.target.checked; chrome.storage.local.set({ autoMark: autoMark }); if (autoMark) { updateStatus('auto-mark enabled', '#0f0'); setupAutoMark(); } else { updateStatus('auto-mark disabled', '#666'); removeAutoMark(); } } function setupAutoMark() { const canvas = document.querySelector('canvas'); if (!canvas) { setTimeout(setupAutoMark, 500); return; } canvas.addEventListener('click', handleAutoMark); } function removeAutoMark() { const canvas = document.querySelector('canvas'); if (canvas) { canvas.removeEventListener('click', handleAutoMark); } } function handleAutoMark(e) { if (!autoMark || !ws || ws.readyState !== WebSocket.OPEN || gameStarted) return; const canvas = e.target; const rect = canvas.getBoundingClientRect(); const canvasX = e.clientX - rect.left; const canvasY = e.clientY - rect.top; const centerX = (canvasX - cameraState.gameWidth / 2) / cameraState.scale + cameraState.offsetX; const centerY = (canvasY - cameraState.gameHeight / 2) / cameraState.scale + cameraState.offsetY; const gameX = centerX + cameraState.gameWidth / 2; const gameY = centerY + cameraState.gameHeight / 2; const cellX = Math.floor(gameX); const cellY = Math.floor(gameY); ws.send(JSON.stringify({ type: 'location', x: cellX, y: cellY })); } function updateStatus(status, color = '#666') { const statusEl = document.querySelector('.tracker-status'); if (statusEl) { statusEl.textContent = status; statusEl.style.color = color; } } function updatePlayers(players) { const playersEl = document.getElementById('tracker-players'); if (!playersEl) return; playersEl.innerHTML = ''; spawnPositions.clear(); players.forEach(player => { const playerDiv = document.createElement('div'); playerDiv.className = 'tracker-player'; let locationText = 'waiting'; if (player.location) { spawnPositions.set(player.name, { x: player.location.x, y: player.location.y }); locationText = `tile (${Math.round(player.location.x)}, ${Math.round(player.location.y)})`; } playerDiv.innerHTML = ` ${player.name} ${locationText} `; playersEl.appendChild(playerDiv); }); } function connectWebSocket(serverUrl, room, name) { if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } roomId = room; playerName = name; updateStatus('connecting', '#888'); ws = new WebSocket(serverUrl); ws.onopen = () => { updateStatus('connected', '#0f0'); ws.send(JSON.stringify({ type: 'join', roomId: roomId, playerName: playerName })); chrome.storage.local.get(['autoMark'], (data) => { if (data.autoMark) { autoMark = true; setupAutoMark(); } }); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'update') { updatePlayers(data.players); } }; ws.onclose = () => { updateStatus('disconnected', '#f00'); removeAutoMark(); setTimeout(() => { if (isEnabled) { connectWebSocket(serverUrl, roomId, playerName); } }, 3000); }; ws.onerror = (error) => { console.error('ws error:', error); updateStatus('error', '#f00'); }; } function clearSpawn() { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'clear' })); updateStatus('cleared', '#666'); } } function startSpawnPhaseCheck() { spawnPhaseCheckInterval = setInterval(() => { const spawnTimer = document.querySelector('spawn-timer'); if (spawnTimer && spawnTimer.shadowRoot) { const timerText = spawnTimer.shadowRoot.textContent; if (!timerText || timerText.includes('Game') || timerText.includes('Tick')) { if (!gameStarted) { gameStarted = true; updateStatus('game started', '#888'); } } } }, 1000); } function getUsername() { return localStorage.getItem('username') || ''; } chrome.storage.sync.get(['serverUrl', 'roomId', 'playerName', 'enabled', 'autoMark'], (data) => { const serverUrl = data.serverUrl || 'wss://op.lopensed.dev'; const roomId = data.roomId || 'default'; const name = data.playerName || getUsername(); const enabled = data.enabled !== false; chrome.storage.sync.set({ serverUrl: serverUrl, roomId: roomId, enabled: enabled }); if (enabled && serverUrl && roomId && name) { isEnabled = true; createOverlay(); connectWebSocket(serverUrl, roomId, name); } }); chrome.storage.onChanged.addListener((changes, namespace) => { if (namespace === 'sync') { chrome.storage.sync.get(['serverUrl', 'roomId', 'playerName', 'enabled'], (data) => { const name = data.playerName || getUsername(); if (data.enabled && data.serverUrl && data.roomId && name) { isEnabled = true; if (!document.getElementById('spawn-tracker-overlay')) { createOverlay(); } connectWebSocket(data.serverUrl, data.roomId, name); } else { isEnabled = false; if (ws) ws.close(); removeAutoMark(); if (spawnPhaseCheckInterval) clearInterval(spawnPhaseCheckInterval); const overlay = document.getElementById('spawn-tracker-overlay'); if (overlay) overlay.remove(); if (overlayCanvas) overlayCanvas.remove(); } }); } });