Greasy Fork is available in English.
Advanced WorldGuessr overlay toolkit - lat/long bands, offset radius circle, country outlines, and exact pin marker. Left Shift to open.
当前为
// ==UserScript==
// @name WorldGuessr GeoAssist
// @namespace http://greasyfork.icu/
// @version 1.1
// @description Advanced WorldGuessr overlay toolkit - lat/long bands, offset radius circle, country outlines, and exact pin marker. Left Shift to open.
// @author WhosGravy
// @match *://www.worldguessr.com/*
// @match *://worldguessr.com/*
// @icon data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0%25' stop-color='%2322d3ee'/><stop offset='100%25' stop-color='%23818cf8'/></linearGradient></defs><rect x='4' y='4' width='56' height='56' rx='16' fill='url(%23g)'/><path fill='%23e2e8f0' d='M32 14a14 14 0 0 0-14 14c0 8.8 11 22.4 12.2 23.9a2.5 2.5 0 0 0 3.7 0C35 50.4 46 36.8 46 28a14 14 0 0 0-14-14zm0 19.2a5.2 5.2 0 1 1 0-10.4 5.2 5.2 0 0 1 0 10.4z'/></svg>
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const _Proxy = window.Proxy;
const _Reflect = window.Reflect;
const SCRIPT_VERSION = '1.1';
const PIN_GLYPH_SVG = '<svg viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false"><path fill="#e2e8f0" d="M12 2.2a7.2 7.2 0 0 0-7.2 7.2c0 4.5 5.6 11.4 6.2 12.2a1.3 1.3 0 0 0 2 0c.6-.8 6.2-7.7 6.2-12.2A7.2 7.2 0 0 0 12 2.2zm0 9.9a2.7 2.7 0 1 1 0-5.4 2.7 2.7 0 0 1 0 5.4z"/></svg>';
const MAP_PIN_IMAGE_URL = 'https://i.imgur.com/taGq4EF.png';
const MAP_PIN_SIZE = [28, 34];
const SV_HEAT_TILE_URL = 'https://mt1.google.com/vt?lyrs=svv|cb_client:apiv3&style=40,18&x={x}&y={y}&z={z}';
// Unlock iframes
const _origSetAttr = Element.prototype.setAttribute;
Element.prototype.setAttribute = new _Proxy(_origSetAttr, {
apply(target, thisArg, args) {
if (thisArg.tagName === 'IFRAME' && args[0].toLowerCase() === 'sandbox') return;
return _Reflect.apply(target, thisArg, args);
}
});
// Country ISO-2 pool for decoys
const ALL_COUNTRIES = [
'AF', 'AL', 'DZ', 'AR', 'AM', 'AU', 'AT', 'AZ', 'BD', 'BE', 'BO', 'BA', 'BR', 'BG', 'KH', 'CM',
'CA', 'CL', 'CN', 'CO', 'CR', 'CU', 'CZ', 'DK', 'DO', 'EC', 'EG', 'SV', 'ET', 'FI', 'FR', 'GE',
'DE', 'GH', 'GR', 'GT', 'HN', 'HU', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IL', 'IT', 'JP', 'JO', 'KZ',
'KE', 'KR', 'KW', 'KG', 'LA', 'LB', 'LY', 'MY', 'MX', 'MD', 'MN', 'MA', 'MZ', 'MM', 'NP', 'NL',
'NZ', 'NI', 'NE', 'NG', 'NO', 'PK', 'PA', 'PY', 'PE', 'PH', 'PL', 'PT', 'RO', 'RU', 'SA', 'SN',
'RS', 'ZA', 'ES', 'LK', 'SD', 'SE', 'CH', 'SY', 'TZ', 'TH', 'TN', 'TR', 'UA', 'AE', 'GB', 'US',
'UY', 'UZ', 'VE', 'VN', 'YE', 'ZM', 'ZW'
];
// Persistence
const STORAGE_KEY = 'wg_geoassist_v3';
const DEFAULTS = {
bandsEnabled: true,
mode: 'both',
latHeightKm: 200,
lngWidthKm: 300,
latColor: '#ff5a5a',
lngColor: '#468cff',
circleEnabled: false,
circleRadiusKm: 500,
circleColor: '#22c55e',
countryEnabled: false,
countryDecoys: 2,
countryColor: '#fbbf24',
pinEnabled: false,
svHeatEnabled: false,
svHeatOpacity: 45,
toggleHotkeyCode: '',
toggleHotkeyLabel: 'None',
mapsHotkeyCode: '',
mapsHotkeyLabel: 'None'
};
function loadConfig() {
try { return { ...DEFAULTS, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') }; }
catch { return { ...DEFAULTS }; }
}
function saveConfig() { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); }
let cfg = loadConfig();
// State
let gameMap = null;
let latBand = null;
let lngBand = null;
let latRandFrac = Math.random();
let lngRandFrac = Math.random();
let circleLayer = null;
let circleRandBearing = Math.random() * Math.PI * 2;
let circleRandFrac = 0.15 + Math.sqrt(Math.random()) * 0.75;
let exactPinMarker = null;
let svHeatLayer = null;
let countryLayers = [];
let countryFeatureIndex = null;
let currentIso = null;
let lastCoords = null;
let panel = null;
let activeTab = 'bands';
let coordPollId = null;
let countryBusy = false;
let countryRetryTimer = null;
let countryRetryAttempt = 0;
let countryRunId = 0;
let lastActivatedCheatKeys = [];
let awaitingHotkeyCapture = false;
let awaitingMapsHotkeyCapture = false;
// Coordinate extraction
function getCoordinates() {
try {
const iframe =
document.querySelector('#PanoramaIframe') ||
document.querySelector('iframe[src*="location"]') ||
document.querySelector('.iframeWithStreetView');
if (iframe?.src) {
const loc = new URL(iframe.src).searchParams.get('location');
if (loc) {
const [lat, lng] = loc.split(',').map(Number);
if (isFinite(lat) && isFinite(lng)) return { lat, lng };
}
}
} catch {}
return null;
}
// Unit helpers
const EARTH_RADIUS_KM = 6371;
const COUNTRY_FETCH_DELAY_MS = 60;
const COUNTRY_FETCH_RETRIES = 2;
const COUNTRY_RETRY_DELAY_MS = 1400;
const COUNTRY_AUTO_RETRY_DELAY_MS = 2200;
const COUNTRY_AUTO_RETRY_MAX = 8;
const COUNTRY_DATA_URL = 'https://cdn.jsdelivr.net/gh/datasets/geo-countries@master/data/countries.geojson';
const kmToLatDeg = km => km / 111.32;
const kmToLngDeg = (km, lat) => km / (111.32 * Math.max(Math.cos(lat * Math.PI / 180), 0.001));
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function fetchJsonWithRetry(url, options = {}, retries = COUNTRY_FETCH_RETRIES) {
let lastErr;
for (let i = 0; i <= retries; i++) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error('HTTP ' + res.status);
return await res.json();
} catch (err) {
lastErr = err;
if (i < retries) await sleep(COUNTRY_RETRY_DELAY_MS + i * 500);
}
}
throw lastErr;
}
function offsetCoords(lat, lng, distanceKm, bearingRad) {
const angDist = distanceKm / EARTH_RADIUS_KM;
const lat1 = lat * Math.PI / 180;
const lng1 = lng * Math.PI / 180;
const sinLat1 = Math.sin(lat1);
const cosLat1 = Math.cos(lat1);
const sinAng = Math.sin(angDist);
const cosAng = Math.cos(angDist);
const lat2 = Math.asin(sinLat1 * cosAng + cosLat1 * sinAng * Math.cos(bearingRad));
const lng2 = lng1 + Math.atan2(
Math.sin(bearingRad) * sinAng * cosLat1,
cosAng - sinLat1 * Math.sin(lat2)
);
return {
lat: lat2 * 180 / Math.PI,
lng: ((lng2 * 180 / Math.PI + 540) % 360) - 180
};
}
function hexToRgba(hex, alpha) {
const raw = String(hex || '').trim().replace('#', '');
const full = raw.length === 3 ? raw.split('').map(c => c + c).join('') : raw;
if (!/^[0-9a-fA-F]{6}$/.test(full)) return `rgba(255,255,255,${alpha})`;
const int = parseInt(full, 16);
const r = (int >> 16) & 255;
const g = (int >> 8) & 255;
const b = int & 255;
return `rgba(${r},${g},${b},${alpha})`;
}
// Bands
function clearBands() {
if (!gameMap) return;
if (latBand) { gameMap.removeLayer(latBand); latBand = null; }
if (lngBand) { gameMap.removeLayer(lngBand); lngBand = null; }
}
function drawBands() {
if (!gameMap || !lastCoords) return;
clearBands();
const { lat, lng } = lastCoords;
if (cfg.mode === 'lat' || cfg.mode === 'both') {
const h = kmToLatDeg(cfg.latHeightKm);
const mn = lat - latRandFrac * h;
latBand = L.rectangle([[mn, -540], [mn + h, 540]], {
color: hexToRgba(cfg.latColor, 0.85), weight: 1.5,
fillColor: cfg.latColor, fillOpacity: 0.11,
interactive: false, noClip: true
}).addTo(gameMap);
}
if (cfg.mode === 'lng' || cfg.mode === 'both') {
const w = kmToLngDeg(cfg.lngWidthKm, lat);
const mn = lng - lngRandFrac * w;
lngBand = L.rectangle([[-90, mn], [90, mn + w]], {
color: hexToRgba(cfg.lngColor, 0.85), weight: 1.5,
fillColor: cfg.lngColor, fillOpacity: 0.11,
interactive: false, noClip: true
}).addTo(gameMap);
}
}
// Circle
function clearCircle() {
if (circleLayer && gameMap) { gameMap.removeLayer(circleLayer); circleLayer = null; }
}
function drawCircle() {
if (!gameMap || !lastCoords) return;
clearCircle();
const offsetKm = cfg.circleRadiusKm * circleRandFrac;
const circleCenter = offsetCoords(lastCoords.lat, lastCoords.lng, offsetKm, circleRandBearing);
circleLayer = L.circle([circleCenter.lat, circleCenter.lng], {
radius: cfg.circleRadiusKm * 1000,
color: hexToRgba(cfg.circleColor, 0.95),
weight: 2,
fillColor: cfg.circleColor,
fillOpacity: 0.14,
interactive: false
}).addTo(gameMap);
}
// Exact pin
function clearExactPin() {
if (exactPinMarker && gameMap) {
gameMap.removeLayer(exactPinMarker);
exactPinMarker = null;
}
}
function drawExactPin() {
if (!gameMap || !lastCoords) return;
clearExactPin();
const icon = L.icon({
iconUrl: MAP_PIN_IMAGE_URL,
iconSize: MAP_PIN_SIZE,
iconAnchor: [Math.round(MAP_PIN_SIZE[0] / 2), MAP_PIN_SIZE[1]]
});
exactPinMarker = L.marker([lastCoords.lat, lastCoords.lng], { icon, interactive: false }).addTo(gameMap);
}
// Street View heat overlay
function clearSvHeat() {
if (svHeatLayer && gameMap) {
gameMap.removeLayer(svHeatLayer);
svHeatLayer = null;
}
}
function drawSvHeat() {
if (!gameMap) return;
clearSvHeat();
svHeatLayer = L.tileLayer(SV_HEAT_TILE_URL, {
maxZoom: 20,
opacity: Math.max(0, Math.min(1, (cfg.svHeatOpacity || 45) / 100)),
noWrap: true,
pane: 'overlayPane',
className: 'cga-svheat-layer'
}).addTo(gameMap);
}
function openCurrentLocationInMaps() {
const c = lastCoords || getCoordinates();
if (!c) return;
const url = `https://www.google.com/maps?q=${c.lat},${c.lng}&ll=${c.lat},${c.lng}&z=18&t=k`;
window.open(url, '_blank', 'noopener,noreferrer');
}
// Country outlines
function clearCountryLayers() {
if (!gameMap) return;
countryLayers.forEach(l => { try { gameMap.removeLayer(l); } catch {} });
countryLayers = [];
}
const getCountryStyle = () => ({
color: cfg.countryColor,
weight: 2.5,
fillColor: cfg.countryColor,
fillOpacity: 0.07,
interactive: false
});
function addGeoLayer(data) {
if (!gameMap || !data) return;
try {
const layer = L.geoJSON(data, { style: () => getCountryStyle() }).addTo(gameMap);
countryLayers.push(layer);
} catch (e) { console.warn('[GeoAssist] GeoJSON draw error', e); }
}
function setCountryStatus(msg, color) {
const el = document.getElementById('cga-country-status');
if (el) { el.textContent = msg; el.style.color = color || cfg.countryColor; }
}
function clearCountryRetry() {
if (countryRetryTimer) {
clearTimeout(countryRetryTimer);
countryRetryTimer = null;
}
}
function isCountryRunActive(runId) {
return runId === countryRunId && cfg.countryEnabled && !!gameMap;
}
function scheduleCountryRetry(lat, lng) {
if (!cfg.countryEnabled || !gameMap) return;
if (countryRetryAttempt >= COUNTRY_AUTO_RETRY_MAX) {
setCountryStatus('Failed (retry limit)', '#f87171');
return;
}
clearCountryRetry();
countryRetryAttempt++;
setCountryStatus(`Retrying... ${countryRetryAttempt}/${COUNTRY_AUTO_RETRY_MAX}`, '#fbbf24');
countryRetryTimer = setTimeout(() => {
countryRetryTimer = null;
drawCountryOutlines(lat, lng);
}, COUNTRY_AUTO_RETRY_DELAY_MS);
}
function getFeatureIso2(props) {
if (!props) return null;
const raw =
props['ISO3166-1-Alpha-2'] ||
props.ISO_A2 ||
props.iso_a2 ||
props.ISO2 ||
props.iso2;
if (!raw || raw === '-99') return null;
return String(raw).toUpperCase();
}
async function ensureCountryIndex() {
if (countryFeatureIndex) return countryFeatureIndex;
const data = await fetchJsonWithRetry(COUNTRY_DATA_URL);
const index = new Map();
for (const feature of (data?.features || [])) {
const iso2 = getFeatureIso2(feature?.properties);
if (!iso2) continue;
if (!index.has(iso2)) index.set(iso2, []);
index.get(iso2).push(feature);
}
countryFeatureIndex = index;
return index;
}
function addCountryByIso2(iso2) {
if (!countryFeatureIndex || !iso2) return false;
const features = countryFeatureIndex.get(iso2);
if (!features?.length) return false;
addGeoLayer({ type: 'FeatureCollection', features });
return true;
}
async function resolveCountryIso2(lat, lng) {
try {
const revData = await fetchJsonWithRetry(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=jsonv2&zoom=3&addressdetails=1`,
{ headers: { 'Accept-Language': 'en' } }
);
const iso = revData?.address?.country_code?.toUpperCase();
if (iso) return iso;
} catch {}
try {
const bdcData = await fetchJsonWithRetry(
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lng}&localityLanguage=en`
);
const iso = bdcData?.countryCode?.toUpperCase();
if (iso) return iso;
} catch {}
return null;
}
async function drawCountryOutlines(snapLat, snapLng) {
const runId = ++countryRunId;
countryBusy = true;
clearCountryRetry();
clearCountryLayers();
setCountryStatus('Loading boundaries...', '#94a3b8');
try {
await ensureCountryIndex();
currentIso = await resolveCountryIso2(snapLat, snapLng);
if (!isCountryRunActive(runId)) return;
if (currentIso) addCountryByIso2(currentIso);
const pool = ALL_COUNTRIES.filter(c => c !== currentIso).sort(() => Math.random() - 0.5);
let addedDecoys = 0;
for (const code of pool) {
if (addedDecoys >= cfg.countryDecoys) break;
await sleep(COUNTRY_FETCH_DELAY_MS);
if (!isCountryRunActive(runId)) break;
if (addCountryByIso2(code)) addedDecoys++;
}
if (!isCountryRunActive(runId)) return;
countryRetryAttempt = 0;
setCountryStatus(currentIso ? 'Loaded' : 'Loaded (no exact country)', currentIso ? '#4ade80' : '#fbbf24');
} catch (e) {
console.error('[GeoAssist] Country fetch failed', e);
if (isCountryRunActive(runId)) {
setCountryStatus('Failed, retrying...', '#f87171');
scheduleCountryRetry(snapLat, snapLng);
}
}
if (runId === countryRunId) countryBusy = false;
}
function isCheatEnabled(key) {
if (key === 'bands') return !!cfg.bandsEnabled;
if (key === 'circle') return !!cfg.circleEnabled;
if (key === 'country') return !!cfg.countryEnabled;
if (key === 'pin') return !!cfg.pinEnabled;
if (key === 'svheat') return !!cfg.svHeatEnabled;
return false;
}
function getEnabledCheatKeys() {
return ['bands', 'circle', 'country', 'pin', 'svheat'].filter(isCheatEnabled);
}
function updateLastUsedCheatSet() {
lastActivatedCheatKeys = getEnabledCheatKeys();
}
function syncCheatUi(key) {
if (!panel) return;
if (key === 'bands') {
const tog = panel.querySelector('#cga-bands-tog');
const ctrl = panel.querySelector('#cga-bands-ctrl');
if (tog) tog.checked = !!cfg.bandsEnabled;
if (ctrl) {
ctrl.style.opacity = cfg.bandsEnabled ? '1' : '.35';
ctrl.style.pointerEvents = cfg.bandsEnabled ? '' : 'none';
}
return;
}
if (key === 'circle') {
const tog = panel.querySelector('#cga-circle-tog');
const ctrl = panel.querySelector('#cga-circle-ctrl');
if (tog) tog.checked = !!cfg.circleEnabled;
if (ctrl) {
ctrl.style.opacity = cfg.circleEnabled ? '1' : '.35';
ctrl.style.pointerEvents = cfg.circleEnabled ? '' : 'none';
}
return;
}
if (key === 'country') {
const tog = panel.querySelector('#cga-country-tog');
const ctrl = panel.querySelector('#cga-country-ctrl');
if (tog) tog.checked = !!cfg.countryEnabled;
if (ctrl) {
ctrl.style.opacity = cfg.countryEnabled ? '1' : '.35';
ctrl.style.pointerEvents = cfg.countryEnabled ? '' : 'none';
}
return;
}
if (key === 'pin') {
const tog = panel.querySelector('#cga-pin-tog');
if (tog) tog.checked = !!cfg.pinEnabled;
return;
}
if (key === 'svheat') {
const tog = panel.querySelector('#cga-svheat-tog');
if (tog) tog.checked = !!cfg.svHeatEnabled;
const val = panel.querySelector('#cga-svheat-op-val');
const sl = panel.querySelector('#cga-svheat-op');
const ctrl = panel.querySelector('#cga-svheat-ctrl');
if (val) val.textContent = `${cfg.svHeatOpacity}%`;
if (sl) {
sl.value = cfg.svHeatOpacity;
sl.style.setProperty('--pct', `${cfg.svHeatOpacity}%`);
}
if (ctrl) {
ctrl.style.opacity = cfg.svHeatEnabled ? '1' : '.35';
ctrl.style.pointerEvents = cfg.svHeatEnabled ? '' : 'none';
}
}
}
function setCheatEnabled(key, enabled, markAsActivated = true) {
if (key === 'bands') {
cfg.bandsEnabled = !!enabled;
if (cfg.bandsEnabled) drawBands(); else clearBands();
}
if (key === 'circle') {
cfg.circleEnabled = !!enabled;
if (cfg.circleEnabled) drawCircle(); else clearCircle();
}
if (key === 'country') {
cfg.countryEnabled = !!enabled;
if (cfg.countryEnabled && lastCoords) {
countryRetryAttempt = 0;
drawCountryOutlines(lastCoords.lat, lastCoords.lng);
} else {
clearCountryRetry();
countryRetryAttempt = 0;
countryRunId++;
countryBusy = false;
clearCountryLayers();
setCountryStatus('', '');
}
}
if (key === 'pin') {
cfg.pinEnabled = !!enabled;
if (cfg.pinEnabled) drawExactPin(); else clearExactPin();
}
if (key === 'svheat') {
cfg.svHeatEnabled = !!enabled;
if (cfg.svHeatEnabled) drawSvHeat(); else clearSvHeat();
}
if (markAsActivated) updateLastUsedCheatSet();
saveConfig();
syncCheatUi(key);
}
function formatHotkeyFromEvent(e) {
if (e.code.startsWith('Key')) return e.code.slice(3).toUpperCase();
if (e.code.startsWith('Digit')) return e.code.slice(5);
if (e.code === 'Space') return 'Space';
if (e.code.startsWith('Arrow')) return e.code.replace('Arrow', '');
return e.key.length === 1 ? e.key.toUpperCase() : e.key;
}
function syncHotkeyUi() {
if (!panel) return;
const valEl = panel.querySelector('#cga-hotkey-val');
const setBtn = panel.querySelector('#cga-hotkey-set');
const mapValEl = panel.querySelector('#cga-maps-hotkey-val');
const mapSetBtn = panel.querySelector('#cga-maps-hotkey-set');
if (valEl) valEl.textContent = awaitingHotkeyCapture ? 'Press key...' : (cfg.toggleHotkeyLabel || 'None');
if (setBtn) setBtn.textContent = awaitingHotkeyCapture ? 'Cancel capture' : 'Set key';
if (mapValEl) mapValEl.textContent = awaitingMapsHotkeyCapture ? 'Press key...' : (cfg.mapsHotkeyLabel || 'None');
if (mapSetBtn) mapSetBtn.textContent = awaitingMapsHotkeyCapture ? 'Cancel capture' : 'Set key';
}
function syncOverlayColorUi() {
if (!panel) return;
panel.style.setProperty('--col-lat', cfg.latColor);
panel.style.setProperty('--col-lng', cfg.lngColor);
panel.style.setProperty('--col-circle', cfg.circleColor);
panel.style.setProperty('--col-country', cfg.countryColor);
const latLabel = panel.querySelector('#cga-lat-label');
const lngLabel = panel.querySelector('#cga-lng-label');
if (latLabel) latLabel.style.color = cfg.latColor;
if (lngLabel) lngLabel.style.color = cfg.lngColor;
const bandsTog = panel.querySelector('#cga-tog-bands-wrap');
const circleTog = panel.querySelector('#cga-tog-circle-wrap');
const countryTog = panel.querySelector('#cga-tog-country-wrap');
if (bandsTog) bandsTog.style.setProperty('--acc', cfg.latColor);
if (circleTog) circleTog.style.setProperty('--acc', cfg.circleColor);
if (countryTog) countryTog.style.setProperty('--acc', cfg.countryColor);
const latSl = panel.querySelector('#cga-lat-sl');
const lngSl = panel.querySelector('#cga-lng-sl');
const circleSl = panel.querySelector('#cga-radius-sl');
if (latSl) latSl.style.setProperty('--acc', cfg.latColor);
if (lngSl) lngSl.style.setProperty('--acc', cfg.lngColor);
if (circleSl) circleSl.style.setProperty('--acc', cfg.circleColor);
}
function toggleMostRecentCheat() {
const targets = lastActivatedCheatKeys.length ? [...lastActivatedCheatKeys] : getEnabledCheatKeys();
if (!targets.length) return;
const allEnabled = targets.every(isCheatEnabled);
for (const key of targets) setCheatEnabled(key, !allEnabled, false);
}
// Master update on new location
function onNewCoords(coords) {
lastCoords = coords;
latRandFrac = Math.random();
lngRandFrac = Math.random();
circleRandBearing = Math.random() * Math.PI * 2;
circleRandFrac = 0.15 + Math.sqrt(Math.random()) * 0.75;
if (cfg.bandsEnabled) drawBands(); else clearBands();
if (cfg.circleEnabled) drawCircle(); else clearCircle();
if (cfg.pinEnabled) drawExactPin(); else clearExactPin();
if (cfg.svHeatEnabled) drawSvHeat(); else clearSvHeat();
if (cfg.countryEnabled) {
countryRetryAttempt = 0;
drawCountryOutlines(coords.lat, coords.lng);
} else {
clearCountryRetry();
countryRetryAttempt = 0;
countryRunId++;
countryBusy = false;
clearCountryLayers();
}
}
// Leaflet map interception
const mapObserver = new MutationObserver(() => {
try {
if (typeof L !== 'undefined' && L.Map?.prototype.setView) {
const origSetView = L.Map.prototype.setView;
L.Map.prototype.setView = new _Proxy(origSetView, {
apply(target, thisArg, args) {
if (!gameMap) {
gameMap = thisArg;
if (lastCoords) {
if (cfg.bandsEnabled) drawBands();
if (cfg.circleEnabled) drawCircle();
if (cfg.pinEnabled) drawExactPin();
if (cfg.svHeatEnabled) drawSvHeat();
}
}
return _Reflect.apply(target, thisArg, args);
}
});
mapObserver.disconnect();
}
} catch {}
});
mapObserver.observe(document.body, { childList: true, subtree: true });
// Coordinate polling
const iframeObserver = new MutationObserver(() => {
const iframe =
document.querySelector('#PanoramaIframe') ||
document.querySelector('iframe[src*="location"]') ||
document.querySelector('.iframeWithStreetView');
if (iframe) {
iframeObserver.disconnect();
if (coordPollId) clearInterval(coordPollId);
coordPollId = setInterval(() => {
const c = getCoordinates();
if (!c) return;
if (!lastCoords || lastCoords.lat !== c.lat || lastCoords.lng !== c.lng) onNewCoords(c);
}, 500);
}
});
iframeObserver.observe(document.body, { childList: true, subtree: true });
// Panel styles
const PANEL_CSS = `
#cga-panel {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%,-50%);
transform-origin: top left;
scale: var(--cga-scale, 1);
zoom: var(--cga-zoom, 1);
z-index: 2147483647;
width: 392px;
max-height: none;
display: flex;
flex-direction: column;
background: #090c10;
border: 1px solid rgba(255,255,255,0.07);
border-radius: 18px;
color: #94a3b8;
font-family: 'DM Sans', 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
box-shadow:
0 40px 100px rgba(0,0,0,0.85),
0 0 0 1px rgba(255,255,255,0.035) inset,
0 1px 0 rgba(255,255,255,0.06) inset;
overflow: hidden;
transition: left 0.22s ease, top 0.22s ease, opacity 0.18s ease, height 0.2s ease;
}
#cga-panel.cga-dragging { transition: none; }
#cga-panel.cga-enter { animation: cga-fade-in 0.18s ease-out; }
#cga-panel.cga-exit { animation: cga-fade-out 0.16s ease-in forwards; }
@keyframes cga-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes cga-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
#cga-panel * { box-sizing: border-box; margin: 0; padding: 0; }
#cga-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 14px;
border-bottom: 1px solid rgba(255,255,255,0.05);
cursor: grab;
flex-shrink: 0;
background: linear-gradient(to bottom, rgba(255,255,255,0.025), transparent);
}
#cga-header:active { cursor: grabbing; }
#cga-logo { display: flex; align-items: center; gap: 9px; }
#cga-logo-mark {
width: 30px; height: 30px;
background: linear-gradient(135deg,#22d3ee 0%,#818cf8 100%);
border-radius: 9px;
display: flex; align-items: center; justify-content: center;
font-size: 15px;
box-shadow: 0 0 16px rgba(34,211,238,0.25);
}
#cga-logo-mark svg { width: 18px; height: 18px; display: block; }
.cga-map-pin-icon { background: transparent; border: 0; }
.cga-map-pin-wrap {
width: 30px; height: 30px;
background: linear-gradient(135deg,#22d3ee 0%,#818cf8 100%);
border-radius: 9px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 0 12px rgba(34,211,238,0.45);
}
.cga-map-pin-wrap svg { width: 18px; height: 18px; display: block; }
#cga-logo-name {
font-size: 15px; font-weight: 700;
letter-spacing: 0.05em; text-transform: uppercase;
color: #e2e8f0;
}
#cga-logo-ver {
font-size: 10px; color: #334155;
letter-spacing: 0.06em; margin-left: 5px; font-weight: 600;
}
#cga-close {
width: 28px; height: 28px;
border-radius: 7px;
display: flex; align-items: center; justify-content: center;
font-size: 15px; color: #334155;
cursor: pointer; transition: all 0.15s;
border: 1px solid transparent;
}
#cga-close:hover { color: #f87171; border-color: rgba(248,113,113,0.25); background: rgba(248,113,113,0.07); }
#cga-tabs {
display: grid; grid-template-columns: repeat(4,1fr);
border-bottom: 1px solid rgba(255,255,255,0.05);
flex-shrink: 0;
}
.cga-tab {
padding: 11px 0 10px;
text-align: center;
font-size: 11.5px; font-weight: 700;
letter-spacing: 0.07em; text-transform: uppercase;
color: #2d3748; cursor: pointer;
transition: all 0.15s;
border-bottom: 2px solid transparent;
position: relative; top: 1px;
}
.cga-tab::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -2px;
height: 2px;
opacity: 0;
}
.cga-tab:hover { color: #4a5568; }
.cga-tab[data-tab="bands"].active {
color: transparent;
border-color: transparent;
background: linear-gradient(90deg, var(--col-lat,#ff7070) 0%, var(--col-lng,#60a5fa) 100%);
-webkit-background-clip: text;
background-clip: text;
}
.cga-tab[data-tab="bands"].active::after {
opacity: 1;
background: linear-gradient(90deg, var(--col-lat,#ff7070) 0%, var(--col-lng,#60a5fa) 100%);
}
.cga-tab[data-tab="circle"].active { color: var(--col-circle,#22c55e); border-color: var(--col-circle,#22c55e); }
.cga-tab[data-tab="country"].active { color: var(--col-country,#fbbf24); border-color: var(--col-country,#fbbf24); }
.cga-tab[data-tab="pin"].active { color: #60a5fa; border-color: #60a5fa; }
#cga-body {
flex: 1;
overflow: hidden;
}
.cga-tab-body { display: none; padding: 18px 20px 14px; }
.cga-tab-body.active {
display: block;
animation: cga-tab-in 0.18s cubic-bezier(0.16,1,0.3,1);
}
@keyframes cga-tab-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.cga-feat-head {
display: flex; align-items: center;
justify-content: flex-end;
min-height: 28px;
margin: 12px 0 14px;
}
.cga-feat-label {
display: flex; align-items: center; gap: 8px;
font-size: 12px; font-weight: 700;
letter-spacing: 0.08em; text-transform: uppercase;
color: #e2e8f0;
}
.cga-feat-icon { font-size: 16px; line-height: 1; }
.cga-tog { position: relative; width: 46px; height: 25px; cursor: pointer; flex-shrink: 0; }
.cga-tog input { opacity: 0; width: 0; height: 0; position: absolute; }
.cga-tog-track {
position: absolute; inset: 0;
border-radius: 12px;
background: #1a2035;
border: 1px solid rgba(255,255,255,0.07);
transition: all 0.22s;
}
.cga-tog input:checked ~ .cga-tog-track {
background: var(--acc,#22d3ee);
border-color: var(--acc,#22d3ee);
box-shadow: 0 0 12px color-mix(in srgb,var(--acc,#22d3ee) 40%,transparent);
}
.cga-tog-knob {
position: absolute; width: 18px; height: 18px;
border-radius: 50%; background: #3a4560;
top: 3px; left: 3px;
transition: all 0.22s;
box-shadow: 0 1px 4px rgba(0,0,0,0.5);
}
.cga-tog input:checked ~ .cga-tog-track .cga-tog-knob { left: 25px; background: #fff; }
.cga-hr { border: none; border-top: 1px solid rgba(255,255,255,0.04); margin: 14px 0; }
.cga-stack { display: flex; flex-direction: column; gap: 15px; }
.cga-srow-top {
display: flex; justify-content: space-between; align-items: baseline;
margin-bottom: 8px;
}
.cga-slabel { font-size: 11.5px; font-weight: 600; color: #4a5568; letter-spacing: 0.04em; }
.cga-sval {
font-size: 12px; font-weight: 700; color: #e2e8f0;
font-family: 'JetBrains Mono','Courier New',monospace;
}
.cga-range {
-webkit-appearance: none; appearance: none;
width: 100%; height: 18px; border-radius: 3px;
background: linear-gradient(to right, var(--acc,#22d3ee) var(--pct,50%), #1a2035 var(--pct,50%));
background-size: 100% 3px;
background-repeat: no-repeat;
background-position: center;
outline: none; cursor: pointer;
}
.cga-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px; height: 15px; border-radius: 50%;
background: var(--acc,#22d3ee);
cursor: pointer;
box-shadow: 0 0 0 3px color-mix(in srgb,var(--acc,#22d3ee) 20%,transparent), 0 2px 6px rgba(0,0,0,0.5);
transition: transform 0.1s;
}
.cga-range:active::-webkit-slider-thumb { transform: scale(1.2); }
.cga-range::-moz-range-thumb {
width: 15px; height: 15px; border: 0; border-radius: 50%;
background: var(--acc,#22d3ee);
cursor: pointer;
box-shadow: 0 0 0 3px color-mix(in srgb,var(--acc,#22d3ee) 20%,transparent), 0 2px 6px rgba(0,0,0,0.5);
}
.cga-range::-moz-range-track {
height: 3px;
background: #1a2035;
border: 0;
border-radius: 3px;
}
.cga-mode-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 5px; }
.cga-mbtn {
padding: 7px 0; border-radius: 7px;
border: 1px solid #1a2035; background: transparent;
color: #2d3748; font-size: 11px; font-weight: 700;
cursor: pointer; text-align: center;
letter-spacing: 0.05em; text-transform: uppercase;
transition: all 0.15s; font-family: inherit;
}
.cga-mbtn[data-mode="lat"] { color: var(--col-lat,#ff7070); border-color: color-mix(in srgb, var(--col-lat,#ff7070) 35%, transparent); background: color-mix(in srgb, var(--col-lat,#ff7070) 10%, transparent); }
.cga-mbtn[data-mode="lng"] { color: var(--col-lng,#60a5fa); border-color: color-mix(in srgb, var(--col-lng,#60a5fa) 35%, transparent); background: color-mix(in srgb, var(--col-lng,#60a5fa) 10%, transparent); }
.cga-mbtn[data-mode="both"] { color: #c4b5fd; border-color: rgba(196,181,253,0.35); background: rgba(196,181,253,0.06); }
.cga-mbtn[data-mode="lat"]:hover { border-color: color-mix(in srgb, var(--col-lat,#ff7070) 60%, transparent); background: color-mix(in srgb, var(--col-lat,#ff7070) 18%, transparent); }
.cga-mbtn[data-mode="lng"]:hover { border-color: color-mix(in srgb, var(--col-lng,#60a5fa) 60%, transparent); background: color-mix(in srgb, var(--col-lng,#60a5fa) 18%, transparent); }
.cga-mbtn[data-mode="both"]:hover { border-color: rgba(196,181,253,0.6); background: rgba(196,181,253,0.12); }
.cga-mbtn[data-mode="lat"].on { background: color-mix(in srgb, var(--col-lat,#ff7070) 25%, transparent); border-color: color-mix(in srgb, var(--col-lat,#ff7070) 75%, transparent); color: var(--col-lat,#ff7070); }
.cga-mbtn[data-mode="lng"].on { background: color-mix(in srgb, var(--col-lng,#60a5fa) 25%, transparent); border-color: color-mix(in srgb, var(--col-lng,#60a5fa) 75%, transparent); color: var(--col-lng,#60a5fa); }
.cga-mbtn[data-mode="both"].on {
color: #e2e8f0;
border-color: rgba(167,139,250,0.75);
background: linear-gradient(90deg, color-mix(in srgb, var(--col-lat,#ff7070) 25%, transparent), color-mix(in srgb, var(--col-lng,#60a5fa) 25%, transparent));
}
.cga-stepper {
display: flex; align-items: center; gap: 8px;
background: #0d1117; border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px; padding: 8px 12px;
}
.cga-sbtn {
width: 24px; height: 24px; border-radius: 6px;
border: 1px solid rgba(255,255,255,0.07);
background: rgba(255,255,255,0.04); color: #6b7280;
font-size: 17px; font-weight: 700; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.12s; line-height: 1; user-select: none;
}
.cga-sbtn:hover { background: rgba(255,255,255,0.09); color: #e2e8f0; }
.cga-snum {
flex: 1; text-align: center; font-weight: 700; color: #e2e8f0;
font-size: 16px; font-family: 'JetBrains Mono',monospace;
}
.cga-sunit { font-size: 11px; color: #2d3748; letter-spacing: 0.04em; }
.cga-status-row {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 8px;
}
#cga-country-status { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--col-country,#fbbf24); min-height: 14px; }
.cga-btn {
width: 100%; padding: 9px 12px; border-radius: 9px;
border: 1px solid rgba(255,255,255,0.06);
background: rgba(255,255,255,0.025); color: #4a5568;
font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; cursor: pointer;
transition: all 0.15s; font-family: inherit;
}
.cga-btn:hover { background: rgba(255,255,255,0.06); color: #94a3b8; border-color: rgba(255,255,255,0.1); }
.cga-color-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.cga-color-input {
width: 34px;
height: 24px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 6px;
background: transparent;
cursor: pointer;
padding: 0;
}
.cga-color-input::-webkit-color-swatch-wrapper { padding: 0; }
.cga-color-input::-webkit-color-swatch { border: 0; border-radius: 5px; }
.cga-svheat-layer { filter: saturate(1.3) contrast(1.15); }
#cga-hint {
padding: 9px 18px 13px; text-align: center;
font-size: 9.5px; letter-spacing: 0.12em;
color: #161e2a; text-transform: uppercase;
border-top: 1px solid rgba(255,255,255,0.03);
flex-shrink: 0;
}
`;
// Panel builder
function buildPanel() {
if (!document.getElementById('cga-fonts')) {
const lnk = document.createElement('link');
lnk.id = 'cga-fonts';
lnk.rel = 'stylesheet';
lnk.href = 'https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap';
document.head.appendChild(lnk);
}
const styleEl = document.createElement('style');
styleEl.id = 'cga-styles';
styleEl.textContent = PANEL_CSS;
document.head.appendChild(styleEl);
const el = document.createElement('div');
el.id = 'cga-panel';
el.innerHTML = `
<div id="cga-header">
<div id="cga-logo">
<div id="cga-logo-mark" aria-label="GeoAssist logo">
${PIN_GLYPH_SVG}
</div>
<div>
<span id="cga-logo-name">GeoAssist</span>
<span id="cga-logo-ver">v${SCRIPT_VERSION}</span>
</div>
</div>
<div id="cga-close">X</div>
</div>
<div id="cga-tabs">
<div class="cga-tab${activeTab === 'bands' ? ' active' : ''}" data-tab="bands">Lat/Long</div>
<div class="cga-tab${activeTab === 'circle' ? ' active' : ''}" data-tab="circle">Circle</div>
<div class="cga-tab${activeTab === 'country' ? ' active' : ''}" data-tab="country">Country</div>
<div class="cga-tab${activeTab === 'pin' ? ' active' : ''}" data-tab="pin">Misc</div>
</div>
<div id="cga-body">
<div class="cga-tab-body${activeTab === 'bands' ? ' active' : ''}" data-body="bands">
<div class="cga-feat-head">
<label class="cga-tog" id="cga-tog-bands-wrap" style="--acc:${cfg.latColor}">
<input type="checkbox" id="cga-bands-tog"${cfg.bandsEnabled ? ' checked' : ''}>
<div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
</label>
</div>
<div id="cga-bands-ctrl" class="cga-stack" style="${cfg.bandsEnabled ? '' : 'opacity:.35;pointer-events:none'}">
<div>
<div class="cga-slabel" style="margin-bottom:8px">Display mode</div>
<div class="cga-mode-grid">
<div class="cga-mbtn${cfg.mode === 'lat' ? ' on' : ''}" data-mode="lat">Lat</div>
<div class="cga-mbtn${cfg.mode === 'both' ? ' on' : ''}" data-mode="both">Both</div>
<div class="cga-mbtn${cfg.mode === 'lng' ? ' on' : ''}" data-mode="lng">Lng</div>
</div>
</div>
<hr class="cga-hr" style="margin:2px 0">
<div>
<div class="cga-srow-top">
<span class="cga-slabel" id="cga-lat-label" style="color:${cfg.latColor}">Latitude height</span>
<span class="cga-sval" id="cga-lat-val">${cfg.latHeightKm} km</span>
</div>
<input class="cga-range" type="range" id="cga-lat-sl"
style="--acc:${cfg.latColor};--pct:${((cfg.latHeightKm - 50) / 4950 * 100).toFixed(1)}%"
min="50" max="5000" step="50" value="${cfg.latHeightKm}">
</div>
<div>
<div class="cga-srow-top">
<span class="cga-slabel" id="cga-lng-label" style="color:${cfg.lngColor}">Longitude width</span>
<span class="cga-sval" id="cga-lng-val">${cfg.lngWidthKm} km</span>
</div>
<input class="cga-range" type="range" id="cga-lng-sl"
style="--acc:${cfg.lngColor};--pct:${((cfg.lngWidthKm - 50) / 4950 * 100).toFixed(1)}%"
min="50" max="5000" step="50" value="${cfg.lngWidthKm}">
</div>
</div>
</div>
<div class="cga-tab-body${activeTab === 'circle' ? ' active' : ''}" data-body="circle">
<div class="cga-feat-head">
<label class="cga-tog" id="cga-tog-circle-wrap" style="--acc:${cfg.circleColor}">
<input type="checkbox" id="cga-circle-tog"${cfg.circleEnabled ? ' checked' : ''}>
<div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
</label>
</div>
<div id="cga-circle-ctrl" class="cga-stack" style="${cfg.circleEnabled ? '' : 'opacity:.35;pointer-events:none'}">
<div>
<div class="cga-srow-top">
<span class="cga-slabel">Radius</span>
<span class="cga-sval" id="cga-radius-val">${cfg.circleRadiusKm} km</span>
</div>
<input class="cga-range" type="range" id="cga-radius-sl"
style="--acc:${cfg.circleColor};--pct:${((cfg.circleRadiusKm - 50) / 4950 * 100).toFixed(1)}%"
min="50" max="5000" step="50" value="${cfg.circleRadiusKm}">
</div>
</div>
</div>
<div class="cga-tab-body${activeTab === 'country' ? ' active' : ''}" data-body="country">
<div class="cga-feat-head">
<label class="cga-tog" id="cga-tog-country-wrap" style="--acc:${cfg.countryColor}">
<input type="checkbox" id="cga-country-tog"${cfg.countryEnabled ? ' checked' : ''}>
<div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
</label>
</div>
<div id="cga-country-ctrl" class="cga-stack" style="${cfg.countryEnabled ? '' : 'opacity:.35;pointer-events:none'}">
<div>
<div class="cga-status-row">
<span class="cga-slabel">Decoy countries</span>
<span id="cga-country-status"></span>
</div>
<div class="cga-stepper">
<button class="cga-sbtn" id="cga-dec-minus">-</button>
<span class="cga-snum" id="cga-dec-val">${cfg.countryDecoys}</span>
<span class="cga-sunit">decoys</span>
<button class="cga-sbtn" id="cga-dec-plus">+</button>
</div>
</div>
<button class="cga-btn" id="cga-country-refetch">Refetch outlines</button>
</div>
</div>
<div class="cga-tab-body${activeTab === 'pin' ? ' active' : ''}" data-body="pin">
<div class="cga-feat-head">
<label class="cga-tog" style="--acc:#60a5fa">
<input type="checkbox" id="cga-pin-tog"${cfg.pinEnabled ? ' checked' : ''}>
<div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
</label>
</div>
<div class="cga-stack">
<div class="cga-slabel">Pin exact location on the map</div>
<hr class="cga-hr" style="margin:4px 0">
<div class="cga-feat-head" style="margin:0 0 8px; min-height:24px; justify-content:space-between; width:100%;">
<span class="cga-slabel">Street View heat overlay</span>
<label class="cga-tog" style="--acc:#ef4444">
<input type="checkbox" id="cga-svheat-tog"${cfg.svHeatEnabled ? ' checked' : ''}>
<div class="cga-tog-track"><div class="cga-tog-knob"></div></div>
</label>
</div>
<div id="cga-svheat-ctrl" style="${cfg.svHeatEnabled ? '' : 'opacity:.35;pointer-events:none'}">
<div class="cga-srow-top">
<span class="cga-slabel">Opacity</span>
<span class="cga-sval" id="cga-svheat-op-val">${cfg.svHeatOpacity}%</span>
</div>
<input class="cga-range" type="range" id="cga-svheat-op" style="--acc:#ef4444;--pct:${cfg.svHeatOpacity}%" min="0" max="100" step="1" value="${cfg.svHeatOpacity}">
</div>
<hr class="cga-hr" style="margin:4px 0">
<div class="cga-srow-top" style="margin-bottom:2px">
<span class="cga-slabel">Last-cheat toggle key</span>
<span class="cga-sval" id="cga-hotkey-val">${cfg.toggleHotkeyLabel || 'None'}</span>
</div>
<button class="cga-btn" id="cga-hotkey-set">Set key</button>
<button class="cga-btn" id="cga-hotkey-clear">Clear key</button>
<div class="cga-srow-top" style="margin-bottom:2px">
<span class="cga-slabel">Open location in maps</span>
<span class="cga-sval" id="cga-maps-hotkey-val">${cfg.mapsHotkeyLabel || 'None'}</span>
</div>
<button class="cga-btn" id="cga-maps-hotkey-set">Set key</button>
<button class="cga-btn" id="cga-maps-hotkey-clear">Clear key</button>
<hr class="cga-hr" style="margin:4px 0">
<div class="cga-color-row"><span class="cga-slabel">Latitude overlay</span><input class="cga-color-input" type="color" id="cga-col-lat" value="${cfg.latColor}"></div>
<div class="cga-color-row"><span class="cga-slabel">Longitude overlay</span><input class="cga-color-input" type="color" id="cga-col-lng" value="${cfg.lngColor}"></div>
<div class="cga-color-row"><span class="cga-slabel">Circle overlay</span><input class="cga-color-input" type="color" id="cga-col-circle" value="${cfg.circleColor}"></div>
<div class="cga-color-row"><span class="cga-slabel">Country overlay</span><input class="cga-color-input" type="color" id="cga-col-country" value="${cfg.countryColor}"></div>
<button class="cga-btn" id="cga-colors-reset">Reset overlay colors</button>
</div>
</div>
</div>
<div id="cga-hint">Left Shift · toggle panel</div>
`;
document.body.appendChild(el);
el.classList.add('cga-enter');
el.querySelector('#cga-close').onclick = () => hidePanel(el);
el.querySelectorAll('.cga-tab').forEach(tab => {
tab.onclick = () => {
switchTabWithResize(el, tab.dataset.tab);
};
});
const bandsTogEl = el.querySelector('#cga-bands-tog');
bandsTogEl.onchange = () => setCheatEnabled('bands', bandsTogEl.checked);
el.querySelectorAll('.cga-mbtn').forEach(btn => {
btn.onclick = () => {
cfg.mode = btn.dataset.mode;
saveConfig();
el.querySelectorAll('.cga-mbtn').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
drawBands();
};
});
bindSlider(el, '#cga-lat-sl', '#cga-lat-val', 'latHeightKm', 50, 5000, drawBands);
bindSlider(el, '#cga-lng-sl', '#cga-lng-val', 'lngWidthKm', 50, 5000, drawBands);
const circleTogEl = el.querySelector('#cga-circle-tog');
circleTogEl.onchange = () => setCheatEnabled('circle', circleTogEl.checked);
bindSlider(el, '#cga-radius-sl', '#cga-radius-val', 'circleRadiusKm', 50, 5000, drawCircle);
const countryTogEl = el.querySelector('#cga-country-tog');
countryTogEl.onchange = () => setCheatEnabled('country', countryTogEl.checked);
el.querySelector('#cga-dec-minus').onclick = () => {
if (cfg.countryDecoys > 0) {
cfg.countryDecoys--;
el.querySelector('#cga-dec-val').textContent = cfg.countryDecoys;
saveConfig();
}
};
el.querySelector('#cga-dec-plus').onclick = () => {
if (cfg.countryDecoys < 25) {
cfg.countryDecoys++;
el.querySelector('#cga-dec-val').textContent = cfg.countryDecoys;
saveConfig();
}
};
el.querySelector('#cga-country-refetch').onclick = () => {
if (!lastCoords) return;
countryRetryAttempt = 0;
clearCountryRetry();
clearCountryLayers();
drawCountryOutlines(lastCoords.lat, lastCoords.lng);
};
const pinTogEl = el.querySelector('#cga-pin-tog');
pinTogEl.onchange = () => setCheatEnabled('pin', pinTogEl.checked);
const svHeatTogEl = el.querySelector('#cga-svheat-tog');
const svHeatOpEl = el.querySelector('#cga-svheat-op');
const svHeatOpValEl = el.querySelector('#cga-svheat-op-val');
svHeatTogEl.onchange = () => setCheatEnabled('svheat', svHeatTogEl.checked);
svHeatOpEl.oninput = () => {
cfg.svHeatOpacity = parseInt(svHeatOpEl.value, 10);
svHeatOpValEl.textContent = `${cfg.svHeatOpacity}%`;
svHeatOpEl.style.setProperty('--pct', `${cfg.svHeatOpacity}%`);
saveConfig();
if (cfg.svHeatEnabled) drawSvHeat();
};
const hotkeySetBtn = el.querySelector('#cga-hotkey-set');
const hotkeyClearBtn = el.querySelector('#cga-hotkey-clear');
const mapsHotkeySetBtn = el.querySelector('#cga-maps-hotkey-set');
const mapsHotkeyClearBtn = el.querySelector('#cga-maps-hotkey-clear');
hotkeySetBtn.onclick = () => {
awaitingMapsHotkeyCapture = false;
awaitingHotkeyCapture = !awaitingHotkeyCapture;
syncHotkeyUi();
};
hotkeyClearBtn.onclick = () => {
awaitingHotkeyCapture = false;
cfg.toggleHotkeyCode = '';
cfg.toggleHotkeyLabel = 'None';
saveConfig();
syncHotkeyUi();
};
mapsHotkeySetBtn.onclick = () => {
awaitingHotkeyCapture = false;
awaitingMapsHotkeyCapture = !awaitingMapsHotkeyCapture;
syncHotkeyUi();
};
mapsHotkeyClearBtn.onclick = () => {
awaitingMapsHotkeyCapture = false;
cfg.mapsHotkeyCode = '';
cfg.mapsHotkeyLabel = 'None';
saveConfig();
syncHotkeyUi();
};
const applyOverlayColor = (key, val) => {
cfg[key] = val;
saveConfig();
syncOverlayColorUi();
if (cfg.bandsEnabled) drawBands();
if (cfg.circleEnabled) drawCircle();
if (cfg.countryEnabled && lastCoords) {
clearCountryLayers();
drawCountryOutlines(lastCoords.lat, lastCoords.lng);
}
};
el.querySelector('#cga-col-lat').oninput = e => applyOverlayColor('latColor', e.target.value);
el.querySelector('#cga-col-lng').oninput = e => applyOverlayColor('lngColor', e.target.value);
el.querySelector('#cga-col-circle').oninput = e => applyOverlayColor('circleColor', e.target.value);
el.querySelector('#cga-col-country').oninput = e => applyOverlayColor('countryColor', e.target.value);
el.querySelector('#cga-colors-reset').onclick = () => {
cfg.latColor = '#ff5a5a';
cfg.lngColor = '#468cff';
cfg.circleColor = '#22c55e';
cfg.countryColor = '#fbbf24';
saveConfig();
syncOverlayColorUi();
el.querySelector('#cga-col-lat').value = cfg.latColor;
el.querySelector('#cga-col-lng').value = cfg.lngColor;
el.querySelector('#cga-col-circle').value = cfg.circleColor;
el.querySelector('#cga-col-country').value = cfg.countryColor;
if (cfg.bandsEnabled) drawBands();
if (cfg.circleEnabled) drawCircle();
if (cfg.countryEnabled && lastCoords) {
clearCountryLayers();
drawCountryOutlines(lastCoords.lat, lastCoords.lng);
}
};
syncHotkeyUi();
syncOverlayColorUi();
syncCheatUi('svheat');
keepPanelInViewport(el);
makeDraggable(el, el.querySelector('#cga-header'));
el.addEventListener('keydown', e => e.stopImmediatePropagation(), true);
el.addEventListener('keyup', e => e.stopImmediatePropagation(), true);
panel = el;
return el;
}
function bindSlider(panelEl, sliderId, valId, key, min, max, redrawFn) {
const slider = panelEl.querySelector(sliderId);
const valEl = panelEl.querySelector(valId);
slider.addEventListener('input', () => {
cfg[key] = parseInt(slider.value, 10);
valEl.textContent = cfg[key] + ' km';
slider.style.setProperty('--pct', ((cfg[key] - min) / (max - min) * 100).toFixed(1) + '%');
saveConfig();
redrawFn();
});
}
function makeDraggable(el, handle) {
let startX, startY, origLeft, origTop;
handle.addEventListener('mousedown', e => {
if (e.target.closest('#cga-close')) return;
e.preventDefault();
el.classList.add('cga-dragging');
const rect = el.getBoundingClientRect();
el.style.transform = 'none';
el.style.left = rect.left + 'px';
el.style.top = rect.top + 'px';
startX = e.clientX; startY = e.clientY;
origLeft = rect.left; origTop = rect.top;
const onMove = e2 => {
el.style.left = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, origLeft + e2.clientX - startX)) + 'px';
el.style.top = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, origTop + e2.clientY - startY)) + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
el.classList.remove('cga-dragging');
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
function applyPanelAutoScale(el) {
if (!el) return;
const safePad = 12;
const availW = Math.max(220, window.innerWidth - safePad * 2);
const availH = Math.max(220, window.innerHeight - safePad * 2);
const currentZoom = Math.max(0.01, parseFloat(el.style.getPropertyValue('--cga-zoom')) || 1);
const rect = el.getBoundingClientRect();
// Convert current on-screen size back to unscaled size, then apply one uniform scale.
const naturalW = Math.max(1, rect.width / currentZoom);
const naturalH = Math.max(1, rect.height / currentZoom);
const fitScale = Math.min(1, availW / naturalW, availH / naturalH);
// Use zoom for consistent shrinking in Chromium-based browsers.
el.style.setProperty('--cga-scale', '1');
el.style.setProperty('--cga-zoom', fitScale.toFixed(4));
}
function keepPanelInViewport(el) {
if (!el || el.style.display === 'none') return;
requestAnimationFrame(() => {
applyPanelAutoScale(el);
const rect = el.getBoundingClientRect();
let nextTop = rect.top;
let nextLeft = rect.left;
let changed = false;
if (rect.bottom > window.innerHeight) {
nextTop = Math.max(0, rect.top - (rect.bottom - window.innerHeight));
changed = true;
}
if (rect.top < 0) {
nextTop = 0;
changed = true;
}
if (rect.right > window.innerWidth) {
nextLeft = Math.max(0, rect.left - (rect.right - window.innerWidth));
changed = true;
}
if (rect.left < 0) {
nextLeft = 0;
changed = true;
}
if (!changed) return;
if (el.style.transform !== 'none') {
el.style.transform = 'none';
el.style.left = rect.left + 'px';
el.style.top = rect.top + 'px';
}
el.style.left = nextLeft + 'px';
el.style.top = nextTop + 'px';
});
}
function showPanel(el) {
if (!el) return;
el.classList.remove('cga-exit');
el.style.display = 'flex';
keepPanelInViewport(el);
// Restart entry animation cleanly.
el.classList.remove('cga-enter');
void el.offsetWidth;
el.classList.add('cga-enter');
}
function switchTabWithResize(el, nextTab) {
if (!el) return;
const startHeight = el.offsetHeight;
activeTab = nextTab;
el.querySelectorAll('.cga-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === nextTab));
el.querySelectorAll('.cga-tab-body').forEach(b => b.classList.toggle('active', b.dataset.body === nextTab));
applyPanelAutoScale(el);
const endHeight = el.offsetHeight;
if (startHeight !== endHeight) {
const rect = el.getBoundingClientRect();
let targetTop = rect.top;
if (rect.top + endHeight > window.innerHeight) {
targetTop = Math.max(0, window.innerHeight - endHeight);
}
el.style.height = startHeight + 'px';
void el.offsetHeight;
el.style.height = endHeight + 'px';
if (targetTop !== rect.top) {
if (el.style.transform !== 'none') {
el.style.transform = 'none';
el.style.left = rect.left + 'px';
el.style.top = rect.top + 'px';
}
el.style.top = targetTop + 'px';
}
const onEnd = () => {
el.style.height = '';
keepPanelInViewport(el);
el.removeEventListener('transitionend', onEnd);
};
el.addEventListener('transitionend', onEnd);
}
keepPanelInViewport(el);
}
function hidePanel(el) {
if (!el || el.style.display === 'none') return;
el.classList.remove('cga-enter');
el.classList.add('cga-exit');
setTimeout(() => {
if (el.classList.contains('cga-exit')) {
el.style.display = 'none';
el.classList.remove('cga-exit');
}
}, 170);
}
document.addEventListener('keydown', e => {
if (awaitingHotkeyCapture || awaitingMapsHotkeyCapture) {
e.preventDefault();
e.stopImmediatePropagation();
if (e.key === 'Escape') {
awaitingHotkeyCapture = false;
awaitingMapsHotkeyCapture = false;
syncHotkeyUi();
return;
}
if (e.key === 'Shift' && e.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) {
return;
}
if (awaitingHotkeyCapture) {
cfg.toggleHotkeyCode = e.code;
cfg.toggleHotkeyLabel = formatHotkeyFromEvent(e);
}
if (awaitingMapsHotkeyCapture) {
cfg.mapsHotkeyCode = e.code;
cfg.mapsHotkeyLabel = formatHotkeyFromEvent(e);
}
awaitingHotkeyCapture = false;
awaitingMapsHotkeyCapture = false;
saveConfig();
syncHotkeyUi();
return;
}
if (e.key === 'Shift' && e.location === KeyboardEvent.DOM_KEY_LOCATION_LEFT) {
e.preventDefault();
e.stopImmediatePropagation();
if (!panel) buildPanel();
else {
const shouldShow = panel.style.display === 'none';
if (shouldShow) showPanel(panel);
else hidePanel(panel);
}
return;
}
if (cfg.toggleHotkeyCode && e.code === cfg.toggleHotkeyCode) {
e.preventDefault();
e.stopImmediatePropagation();
toggleMostRecentCheat();
return;
}
if (cfg.mapsHotkeyCode && e.code === cfg.mapsHotkeyCode) {
e.preventDefault();
e.stopImmediatePropagation();
openCurrentLocationInMaps();
}
}, true);
window.addEventListener('resize', () => keepPanelInViewport(panel));
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(() => {
gameMap = null;
lastCoords = null;
countryBusy = false;
countryRunId++;
clearCountryRetry();
countryRetryAttempt = 0;
exactPinMarker = null;
if (coordPollId) { clearInterval(coordPollId); coordPollId = null; }
iframeObserver.observe(document.body, { childList: true, subtree: true });
mapObserver.observe(document.body, { childList: true, subtree: true });
}, 900);
}
}, 500);
const domWatcher = new MutationObserver(() => {
if (!document.querySelector('.leaflet-container') && gameMap) gameMap = null;
});
domWatcher.observe(document.body, { childList: true, subtree: true });
})();