478 lines
13 KiB
JavaScript
478 lines
13 KiB
JavaScript
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 = `
|
|
<div class="tracker-header" id="tracker-header">
|
|
<span class="tracker-title">spawn tracker</span>
|
|
<button id="tracker-toggle" class="tracker-btn">hide</button>
|
|
</div>
|
|
<div id="tracker-content">
|
|
<div class="tracker-status">disconnected</div>
|
|
<div class="tracker-controls">
|
|
<label class="tracker-checkbox">
|
|
<input type="checkbox" id="auto-mark-toggle">
|
|
<span>auto-mark</span>
|
|
</label>
|
|
</div>
|
|
<div id="tracker-players"></div>
|
|
<button id="tracker-clear" class="tracker-btn tracker-btn-secondary">clear my spawn</button>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<span class="player-name">${player.name}</span>
|
|
<span class="player-location">${locationText}</span>
|
|
`;
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}); |