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 = `
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();
}
});
}
});