From aa67a890e7e2a99d641a0f1214f09cd3d7033966 Mon Sep 17 00:00:00 2001 From: lopensed Date: Tue, 27 Jan 2026 10:50:29 +0100 Subject: [PATCH] Initial commit: OP Extension Chrome Extension --- .gitignore | 2 + content.js | 478 ++++++++++++++++++++++++++++++++++++++++++++++++++ icon128.png | Bin 0 -> 339 bytes icon16.png | Bin 0 -> 299 bytes icon48.png | Bin 0 -> 307 bytes manifest.json | 39 ++++ overlay.css | 145 +++++++++++++++ popup.html | 178 +++++++++++++++++++ popup.js | 96 ++++++++++ 9 files changed, 938 insertions(+) create mode 100644 .gitignore create mode 100644 content.js create mode 100644 icon128.png create mode 100644 icon16.png create mode 100644 icon48.png create mode 100644 manifest.json create mode 100644 overlay.css create mode 100644 popup.html create mode 100644 popup.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2334d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +*.log diff --git a/content.js b/content.js new file mode 100644 index 0000000..79638ab --- /dev/null +++ b/content.js @@ -0,0 +1,478 @@ +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(); + } + }); + } +}); \ No newline at end of file diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..ce856924fa87d69a245aa5de7a975abe838e7a72 GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg76t|e2IZxf#TXbE6p}rHd>I(3)EF2VS{N99 zf#hE>Fq9fFFuY1&V6d9Oz#v{QXIG#N0|Nt7lDE4H1HYA#-C_m?2KEw9Usv{5j7&nj z>^HdH_cJgssFt`!l%yncndurB>KYh@7+P2v8e18dYa19?85pqH zew0Vikei>9nO2EgLwSNH$Wjfs4JDbmsl_FUxdpiOD3oT@FfcHfd%8G=WZZju#*mSL zfrm-K`sV)Gie-FE6XaYK1UOij8Xd4u2frs69ZHD4f4%rBD6~9X{an^LB{Ts5P|{RJ literal 0 HcmV?d00001 diff --git a/icon16.png b/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..3da8b8793a0715d22b2a1246d050174446a0a48f GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd76t|ehW{D9dl(oP6p}rHd>I(3)EF2VS{N99 zF)%PRykKA`HDF+PmB7GYHG_dcykO3*KpO@I2Bsu$cNYeJD)!ag8WRNi0dVN-j!GEJPnaO+Vh&8T5uVBq(3aSY+Oo@~&@ d)OPS71B1>_$;K%x1_wc*;OXk;vd$@?2>=KKN#Fng literal 0 HcmV?d00001 diff --git a/icon48.png b/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..66355e22c989ef6215a9a6377d62436176bcce5a GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-76t|ehV3)GqZk+%6p}rHd>I(3)EF2VS{N99 zF)%PRykKA`HDF+PmB7GYHG_dcykO3*KpO@I2Bsu$cNYeJD)!ag8WRNi0dVN-j!GEJPnaO+Vh&8T5uU=a6oaSX9IoosN7 gQNm3X0@ + + + + + + +

spawn tracker

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
checking server...
+ + + + \ No newline at end of file diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..e69d8c8 --- /dev/null +++ b/popup.js @@ -0,0 +1,96 @@ +// check for auto-detected username +chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { + if (tabs[0] && tabs[0].url && tabs[0].url.includes('openfront.io')) { + chrome.scripting.executeScript({ + target: {tabId: tabs[0].id}, + func: () => localStorage.getItem('username') + }, (results) => { + if (results && results[0] && results[0].result) { + document.getElementById('autoDetected').textContent = `detected: ${results[0].result}`; + } + }); + } +}); + +chrome.storage.sync.get(['serverUrl', 'roomId', 'playerName', 'enabled'], (data) => { + // set defaults + const serverUrl = data.serverUrl || 'wss://op.lopensed.dev'; + const roomId = data.roomId || 'default'; + + document.getElementById('serverUrl').value = serverUrl; + document.getElementById('roomId').value = roomId; + if (data.playerName) document.getElementById('playerName').value = data.playerName; + + // auto-save defaults and enable if not already done + if (!data.enabled || !data.serverUrl || !data.roomId) { + chrome.storage.sync.set({ + serverUrl: serverUrl, + roomId: roomId, + enabled: true + }); + } + + // ping server for debug + pingServer(serverUrl); +}); + +function pingServer(url) { + const debug = document.getElementById('debug'); + debug.textContent = 'checking server...'; + debug.className = 'debug checking'; + + const ws = new WebSocket(url); + + const timeout = setTimeout(() => { + ws.close(); + debug.textContent = 'server: timeout'; + debug.className = 'debug error'; + }, 5000); + + ws.onopen = () => { + clearTimeout(timeout); + debug.textContent = 'server: ok'; + debug.className = 'debug ok'; + setTimeout(() => ws.close(), 100); + }; + + ws.onerror = () => { + clearTimeout(timeout); + debug.textContent = 'server: unreachable'; + debug.className = 'debug error'; + }; +} + +document.getElementById('saveBtn').addEventListener('click', () => { + const serverUrl = document.getElementById('serverUrl').value.trim() || 'wss://op.lopensed.dev'; + const roomId = document.getElementById('roomId').value.trim() || 'default'; + const playerName = document.getElementById('playerName').value.trim(); + + chrome.storage.sync.set({ + serverUrl: serverUrl, + roomId: roomId, + playerName: playerName, + enabled: true + }, () => { + showStatus('saved - refresh page', 'success'); + }); +}); + +document.getElementById('disableBtn').addEventListener('click', () => { + chrome.storage.sync.set({ + enabled: false + }, () => { + showStatus('disabled - refresh page', 'success'); + }); +}); + +function showStatus(message, type) { + const status = document.getElementById('status'); + status.textContent = message; + status.className = `status ${type}`; + status.style.display = 'block'; + + setTimeout(() => { + status.style.display = 'none'; + }, 3000); +} \ No newline at end of file