OpenGuessr cheat

Easy to use location hack/cheat

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         OpenGuessr cheat
// @namespace    monowe
// @version      1.3
// @description  Easy to use location hack/cheat
// @match        https://www.openguessr.com/*
// @match        https://openguessr.com/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // ─── CONFIG ───────────────────────────────────────────────────
    const ANIMATION_DURATION = 4000;
    const MAP_SIZES = {
        small:  { w: 220, h: 160 },
        medium: { w: 280, h: 220 },
        large:  { w: 380, h: 300 },
    };
    const SETTINGS_KEY = 'monowe-settings';
    const defaults = { size: 'medium', theme: 'dark' };
    let settings = { ...defaults };
    try { Object.assign(settings, JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}')); } catch {}
    function saveSettings() { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); }
    function getMapSize() { return MAP_SIZES[settings.size] || MAP_SIZES.medium; }

    // ─── LOAD LEAFLET ─────────────────────────────────────────────
    const leafletCSS = document.createElement('link');
    leafletCSS.rel = 'stylesheet';
    leafletCSS.href = 'https://unpkg.com/[email protected]/dist/leaflet.css';
    (document.head || document.documentElement).appendChild(leafletCSS);

    const leafletJS = document.createElement('script');
    leafletJS.src = 'https://unpkg.com/[email protected]/dist/leaflet.js';
    (document.head || document.documentElement).appendChild(leafletJS);

    // ─── COORDINATE SYSTEM ────────────────────────────────────────
    const listeners = [];
    let lastCoords = null;

    function emitCoords(coords) {
        if (!coords || !isFinite(coords.lat) || !isFinite(coords.lng)) return;
        if (coords.lat === 0 && coords.lng === 0) return;
        if (lastCoords && lastCoords.lat === coords.lat && lastCoords.lng === coords.lng) return;
        lastCoords = coords;
        console.log('[monowe] coords found:', coords.lat, coords.lng);
        for (const fn of listeners) fn(coords);
    }

    // ─── METHOD 1: Hook Google Maps API when it loads ─────────────
    function hookGoogleMapsAPI() {
        const check = setInterval(() => {
            if (!window.google || !window.google.maps) return;
            clearInterval(check);

            console.log('[monowe] Google Maps API detected, hooking...');
            const SVP = window.google.maps.StreetViewPanorama;
            if (!SVP) return;

            // Track all panorama instances
            const instances = new Set();

            // Hook constructor to catch new panorama creations
            const origCtor = SVP;
            const handler = {
                construct(target, args) {
                    const instance = new target(...args);
                    instances.add(instance);
                    hookInstance(instance);
                    // Read initial position
                    setTimeout(() => readPosition(instance), 500);
                    return instance;
                }
            };
            // Replace the constructor
            const proxied = new Proxy(origCtor, handler);
            window.google.maps.StreetViewPanorama = proxied;
            // Copy prototype chain
            proxied.prototype = origCtor.prototype;

            // Hook existing instances found in DOM
            const scanExisting = () => {
                try {
                    const containers = document.querySelectorAll('.gm-style, [aria-roledescription="street view"]');
                    for (const el of containers) {
                        for (const key of Object.keys(el)) {
                            if (key.startsWith('__')) {
                                const obj = el[key];
                                if (obj && typeof obj === 'object' && typeof obj.getPosition === 'function') {
                                    if (!instances.has(obj)) {
                                        instances.add(obj);
                                        hookInstance(obj);
                                    }
                                }
                            }
                        }
                    }
                } catch (e) {}
            };

            function readPosition(pano) {
                try {
                    if (typeof pano.getPosition === 'function') {
                        const pos = pano.getPosition();
                        if (pos) {
                            const lat = typeof pos.lat === 'function' ? pos.lat() : pos.lat;
                            const lng = typeof pos.lng === 'function' ? pos.lng() : pos.lng;
                            if (lat && lng) emitCoords({ lat, lng });
                        }
                    }
                } catch (e) {}
            }

            function hookInstance(pano) {
                // Hook setPosition
                if (typeof pano.setPosition === 'function' && !pano._monoweHooked) {
                    pano._monoweHooked = true;
                    const origSetPos = pano.setPosition.bind(pano);
                    pano.setPosition = function (latLng) {
                        const result = origSetPos(latLng);
                        setTimeout(() => readPosition(pano), 100);
                        return result;
                    };
                }
                // Hook set
                if (typeof pano.set === 'function' && !pano._monoweSetHooked) {
                    pano._monoweSetHooked = true;
                    const origSet = pano.set.bind(pano);
                    pano.set = function (key, value) {
                        const result = origSet(key, value);
                        if (key === 'position') {
                            setTimeout(() => readPosition(pano), 100);
                        }
                        return result;
                    };
                }
            }

            // Periodically scan for new/changed panoramas
            setInterval(() => {
                scanExisting();
                for (const pano of instances) {
                    readPosition(pano);
                }
            }, 2000);

            // Initial scan
            scanExisting();
            for (const pano of instances) {
                readPosition(pano);
            }
        }, 500);
    }

    // ─── METHOD 2: Intercept fetch/XHR (catches API responses) ────
    function hookNetwork() {
        function extractCoords(text) {
            const patterns = [
                /"lat(?:itude)?"\s*:\s*(-?\d+\.?\d*)\s*,\s*"(?:lng|lo(?:ng|n(?:g|itude)?))"\s*:\s*(-?\d+\.?\d*)/i,
                /"(?:lng|lo(?:ng|n(?:g|itude)?))"\s*:\s*(-?\d+\.?\d*)\s*,\s*"lat(?:itude)?"\s*:\s*(-?\d+\.?\d*)/i,
            ];
            for (const re of patterns) {
                const m = text.match(re);
                if (m) {
                    const a = parseFloat(m[1]), b = parseFloat(m[2]);
                    if (Math.abs(a) <= 90 && Math.abs(b) <= 180 && (a !== 0 || b !== 0)) return { lat: a, lng: b };
                }
            }
            return null;
        }

        const origFetch = window.fetch;
        window.fetch = async function (...args) {
            const resp = await origFetch.apply(this, args);
            try {
                const clone = resp.clone();
                clone.text().then((t) => {
                    const c = extractCoords(t);
                    if (c) emitCoords(c);
                }).catch(() => {});
            } catch (e) {}
            return resp;
        };

        const origOpen = XMLHttpRequest.prototype.open;
        const origSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.open = function (m, u, ...r) {
            this._mUrl = u;
            return origOpen.call(this, m, u, ...r);
        };
        XMLHttpRequest.prototype.send = function (...a) {
            this.addEventListener('load', function () {
                try {
                    const c = extractCoords(this.responseText);
                    if (c) emitCoords(c);
                } catch (e) {}
            });
            return origSend.apply(this, a);
        };
    }

    // ─── METHOD 3: Hook JSONP callbacks (Google Maps uses these) ──
    function hookJSONP() {
        // Intercept script tag creation to catch JSONP callbacks
        const origAppendChild = Node.prototype.appendChild;
        Node.prototype.appendChild = function (node) {
            if (node.tagName === 'SCRIPT' && node.src) {
                // Check if it's a Google Maps JSONP call
                if (node.src.includes('maps.googleapis.com') || node.src.includes('callback=')) {
                    const origCb = node.onload;
                    node.addEventListener('load', () => {
                        try {
                            // After JSONP loads, scan for coords in global scope
                            scanGlobalScope();
                        } catch (e) {}
                    });
                }
            }
            return origAppendChild.call(this, node);
        };

        // Hook global callback functions
        const origDefineProperty = Object.defineProperty;
        let callbackCount = 0;
        setInterval(() => {
            scanGlobalScope();
        }, 3000);
    }

    function scanGlobalScope() {
        // Look for Google Maps panorama instances in window
        try {
            for (const key in window) {
                try {
                    const obj = window[key];
                    if (!obj || typeof obj !== 'object') continue;
                    // Check for getPosition
                    if (typeof obj.getPosition === 'function') {
                        const pos = obj.getPosition();
                        if (pos) {
                            const lat = typeof pos.lat === 'function' ? pos.lat() : pos.lat;
                            const lng = typeof pos.lng === 'function' ? pos.lng() : pos.lng;
                            if (lat && lng) emitCoords({ lat, lng });
                        }
                    }
                    // Check for nested pano
                    if (obj.pano && typeof obj.pano.getPosition === 'function') {
                        const pos = obj.pano.getPosition();
                        if (pos) {
                            const lat = typeof pos.lat === 'function' ? pos.lat() : pos.lat;
                            const lng = typeof pos.lng === 'function' ? pos.lng() : pos.lng;
                            if (lat && lng) emitCoords({ lat, lng });
                        }
                    }
                    // Check for position property
                    if (obj.position && typeof obj.position === 'object') {
                        const lat = obj.position.lat;
                        const lng = obj.position.lng;
                        if (lat && lng && isFinite(lat) && isFinite(lng)) {
                            emitCoords({ lat: typeof lat === 'function' ? lat() : lat, lng: typeof lng === 'function' ? lng() : lng });
                        }
                    }
                } catch (e) {}
            }
        } catch (e) {}
    }

    // ─── METHOD 4: MutationObserver on iframes (Street View embed) ─
    function hookIframes() {
        function extractFromUrl(url) {
            if (!url) return;
            // cbll param: Google Maps Street View embed
            const cbll = url.match(/cbll=(-?\d+\.?\d*),(-?\d+\.?\d*)/);
            if (cbll) emitCoords({ lat: parseFloat(cbll[1]), lng: parseFloat(cbll[2]) });
            // location param
            const loc = url.match(/location=(-?\d+\.?\d*),(-?\d+\.?\d*)/);
            if (loc) emitCoords({ lat: parseFloat(loc[1]), lng: parseFloat(loc[2]) });
        }

        const observer = new MutationObserver((mutations) => {
            for (const m of mutations) {
                // Check added nodes
                for (const node of m.addedNodes) {
                    if (node.tagName === 'IFRAME') {
                        extractFromUrl(node.src || node.getAttribute('src') || '');
                    }
                    // Check child iframes
                    if (node.querySelectorAll) {
                        for (const iframe of node.querySelectorAll('iframe')) {
                            extractFromUrl(iframe.src || iframe.getAttribute('src') || '');
                        }
                    }
                }
                // Check attribute changes (iframe src changes on round switch)
                if (m.type === 'attributes' && m.attributeName === 'src' && m.target.tagName === 'IFRAME') {
                    extractFromUrl(m.target.src);
                }
            }
        });
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src'],
        });

        // Also scan existing iframes
        setTimeout(() => {
            for (const iframe of document.querySelectorAll('iframe')) {
                extractFromUrl(iframe.src || iframe.getAttribute('src') || '');
            }
        }, 2000);
    }

    // ─── METHOD 5: Scan page HTML for embedded coordinates ────────
    function scanPageHTML() {
        const html = document.documentElement.innerHTML;
        // Look for Street View embed URLs with coordinates
        const patterns = [
            /cbll=(-?\d+\.?\d*),(-?\d+\.?\d*)/,
            /location=(-?\d+\.?\d*),(-?\d+\.?\d*)/,
            /!3d(-?\d+\.?\d*)!4d(-?\d+\.?\d*)/,
        ];
        for (const re of patterns) {
            const m = html.match(re);
            if (m) {
                emitCoords({ lat: parseFloat(m[1]), lng: parseFloat(m[2]) });
                return;
            }
        }
    }

    // ─── ANIMATION OVERLAY ────────────────────────────────────────
    function showWelcomeAnimation() {
        return new Promise((resolve) => {
            const overlay = document.createElement('div');
            overlay.id = 'monowe-welcome';
            overlay.innerHTML = `
                <canvas id="monowe-particles"></canvas>
                <div class="monowe-text">
                    <span class="monowe-made">Made by</span>
                    <span class="monowe-name">monowe</span>
                </div>
            `;
            document.body.appendChild(overlay);

                    const style = document.createElement('style');
        const sz = getMapSize();
        const c = getThemeColors();
        style.textContent = `
            #monowe-minimap {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 999998;
                width: ${sz.w}px;
                background: ${c.bg};
                border-radius: 12px;
                overflow: hidden;
                box-shadow: ${c.shadow};
                font-family: 'Segoe UI', system-ui, sans-serif;
                backdrop-filter: blur(12px);
                cursor: move;
                user-select: none;
            }
            .monowe-map-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 12px;
                background: ${c.headerBg};
                color: ${c.text};
                font-size: 0.8rem;
                letter-spacing: 0.05em;
            }
            .monowe-header-left {
                display: flex;
                align-items: baseline;
                gap: 6px;
                min-width: 0;
                flex: 1;
            }
            .monowe-header-right {
                display: flex;
                align-items: center;
                gap: 2px;
                flex-shrink: 0;
            }
            .monowe-loc-label {
                color: ${c.textDim};
                font-size: 0.75rem;
                white-space: nowrap;
            }
            .monowe-loc-name {
                color: ${c.locName};
                font-size: 0.78rem;
                font-weight: 600;
                max-width: 140px;
                display: inline-block;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                vertical-align: bottom;
            }
            .monowe-map-header button {
                background: none;
                border: none;
                color: ${c.textDim};
                cursor: pointer;
                font-size: 1.1rem;
                padding: 0 4px;
                line-height: 1;
            }
            .monowe-map-header button:hover {
                color: ${c.btnHover};
            }
            #monowe-map-body {
                height: ${sz.h}px;
                transition: height 0.3s ease;
            }
            #monowe-map-body.collapsed {
                height: 0;
            }
            #monowe-map-body .leaflet-container {
                width: 100%;
                height: 100%;
                background: #1a1a2e;
            }
            #monowe-map-body .leaflet-control-zoom a {
                background: rgba(18,18,18,0.9) !important;
                color: #00d4ff !important;
                border: 1px solid rgba(0,212,255,0.3) !important;
                width: 28px !important;
                height: 28px !important;
                line-height: 28px !important;
                font-size: 16px !important;
            }
            #monowe-map-body .leaflet-control-zoom a:hover {
                background: rgba(0,212,255,0.2) !important;
                color: #fff !important;
            }
        `;
        document.head.appendChild(style);

            const canvas = document.getElementById('monowe-particles');
            const ctx = canvas.getContext('2d');
            let particles = [];
            let animFrame;

            function resizeCanvas() {
                canvas.width = window.innerWidth;
                canvas.height = window.innerHeight;
            }
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);

            for (let i = 0; i < 80; i++) {
                particles.push({
                    x: Math.random() * canvas.width,
                    y: Math.random() * canvas.height,
                    vx: (Math.random() - 0.5) * 0.5,
                    vy: (Math.random() - 0.5) * 0.5,
                    r: Math.random() * 2 + 0.5,
                    a: Math.random() * 0.5 + 0.1,
                });
            }

            function drawParticles() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                for (const p of particles) {
                    p.x += p.vx;
                    p.y += p.vy;
                    if (p.x < 0) p.x = canvas.width;
                    if (p.x > canvas.width) p.x = 0;
                    if (p.y < 0) p.y = canvas.height;
                    if (p.y > canvas.height) p.y = 0;
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255,255,255,${p.a})`;
                    ctx.fill();
                }
                animFrame = requestAnimationFrame(drawParticles);
            }
            drawParticles();

            setTimeout(() => {
                overlay.classList.add('fade-out');
                cancelAnimationFrame(animFrame);
                setTimeout(() => {
                    overlay.remove();
                    resolve();
                }, 800);
            }, ANIMATION_DURATION);
        });
    }

    // ─── MINI-MAP ─────────────────────────────────────────────────
    function getThemeColors() {
        return settings.theme === 'light' ? {
            bg: 'rgba(245,245,250,0.97)',
            headerBg: 'rgba(0,0,0,0.06)',
            text: 'rgba(20,20,30,0.9)',
            textDim: 'rgba(20,20,30,0.5)',
            accent: '#0077cc',
            border: 'rgba(0,0,0,0.1)',
            btnHover: '#0077cc',
            shadow: '0 4px 24px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)',
            locName: '#0077cc',
        } : {
            bg: 'rgba(18,18,18,0.95)',
            headerBg: 'rgba(0,0,0,0.4)',
            text: 'rgba(255,255,255,0.85)',
            textDim: 'rgba(255,255,255,0.45)',
            accent: '#00d4ff',
            border: 'rgba(255,255,255,0.08)',
            btnHover: '#fff',
            shadow: '0 4px 24px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.08)',
            locName: '#00d4ff',
        };
    }

    function applyTheme() {
        const c = getThemeColors();
        const el = document.getElementById('monowe-minimap');
        if (!el) return;
        el.style.background = c.bg;
        el.style.boxShadow = c.shadow;
        const header = el.querySelector('.monowe-map-header');
        if (header) {
            header.style.background = c.headerBg;
            header.style.color = c.text;
        }
        const label = el.querySelector('.monowe-loc-label');
        if (label) label.style.color = c.textDim;
        const locName = el.querySelector('.monowe-loc-name');
        if (locName) locName.style.color = c.locName;
        const btns = el.querySelectorAll('.monowe-map-header button');
        btns.forEach(b => b.style.color = c.textDim);
    }

    function applySize() {
        const sz = getMapSize();
        const el = document.getElementById('monowe-minimap');
        if (!el) return;
        el.style.width = sz.w + 'px';
        const body = document.getElementById('monowe-map-body');
        if (body && !body.classList.contains('collapsed')) body.style.height = sz.h + 'px';
        if (mapObj && mapObj.map) setTimeout(() => mapObj.map.invalidateSize(), 350);
    }

    function toggleSettingsPanel() {
        let panel = document.getElementById('monowe-settings-panel');
        if (panel) { panel.remove(); return; }
        const c = getThemeColors();
        panel = document.createElement('div');
        panel.id = 'monowe-settings-panel';
        panel.innerHTML = `
            <div class="monowe-settings-title">Settings</div>
            <div class="monowe-settings-row">
                <span>Size</span>
                <div class="monowe-size-btns">
                    <button data-size="small" class="${settings.size === 'small' ? 'active' : ''}">S</button>
                    <button data-size="medium" class="${settings.size === 'medium' ? 'active' : ''}">M</button>
                    <button data-size="large" class="${settings.size === 'large' ? 'active' : ''}">L</button>
                </div>
            </div>
            <div class="monowe-settings-row">
                <span>Theme</span>
                <div class="monowe-theme-toggle">
                    <button data-theme="dark" class="${settings.theme === 'dark' ? 'active' : ''}">Dark</button>
                    <button data-theme="light" class="${settings.theme === 'light' ? 'active' : ''}">Light</button>
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        const minimap = document.getElementById('monowe-minimap');
        const rect = minimap.getBoundingClientRect();
        panel.style.position = 'fixed';
        panel.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
        panel.style.right = (window.innerWidth - rect.right) + 'px';

        Object.assign(panel.style, {
            background: c.bg, borderRadius: '10px', padding: '12px 14px',
            zIndex: '999999', boxShadow: c.shadow,
            fontFamily: "'Segoe UI', system-ui, sans-serif", color: c.text, minWidth: '170px',
        });
        panel.querySelector('.monowe-settings-title').style.cssText =
            'font-size:0.7rem;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:10px;color:' + c.textDim;
        panel.querySelectorAll('.monowe-settings-row').forEach(r => {
            r.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;font-size:0.78rem;';
        });
        panel.querySelectorAll('.monowe-settings-row > span').forEach(s => s.style.color = c.textDim);
        panel.querySelectorAll('.monowe-size-btns, .monowe-theme-toggle').forEach(g => {
            g.style.cssText = 'display:flex;gap:4px;';
        });
        panel.querySelectorAll('button').forEach(b => {
            b.style.cssText = `background:${c.headerBg};border:1px solid ${c.border};color:${c.textDim};border-radius:6px;padding:3px 10px;cursor:pointer;font-size:0.72rem;font-family:inherit;transition:all 0.15s;`;
            b.addEventListener('mouseenter', () => { b.style.color = c.text; b.style.borderColor = c.accent; });
            b.addEventListener('mouseleave', () => { if (!b.classList.contains('active')) { b.style.color = c.textDim; b.style.borderColor = c.border; }});
        });
        panel.querySelectorAll('button.active').forEach(b => {
            b.style.background = c.accent;
            b.style.color = settings.theme === 'light' ? '#fff' : '#000';
            b.style.borderColor = c.accent;
        });

        panel.querySelectorAll('[data-size]').forEach(btn => {
            btn.addEventListener('click', () => {
                settings.size = btn.dataset.size;
                saveSettings();
                applySize();
                panel.remove();
                toggleSettingsPanel();
            });
        });
        panel.querySelectorAll('[data-theme]').forEach(btn => {
            btn.addEventListener('click', () => {
                settings.theme = btn.dataset.theme;
                saveSettings();
                applyTheme();
                panel.remove();
                toggleSettingsPanel();
            });
        });

        setTimeout(() => {
            const handler = (e) => {
                if (!panel.contains(e.target) && !e.target.closest('#monowe-settings-btn')) {
                    panel.remove();
                    document.removeEventListener('mousedown', handler);
                }
            };
            document.addEventListener('mousedown', handler);
        }, 50);
    }

    function createMiniMap() {
        const sz = getMapSize();
        const c = getThemeColors();
        const container = document.createElement('div');
        container.id = 'monowe-minimap';
        container.innerHTML = `
            <div class="monowe-map-header">
                <div class="monowe-header-left">
                    <span class="monowe-loc-label">Your Location:</span>
                    <span id="monowe-location-name" class="monowe-loc-name">...</span>
                </div>
                <div class="monowe-header-right">
                    <button id="monowe-settings-btn" title="Settings">\u2699</button>
                    <button id="monowe-map-toggle">\u2212</button>
                </div>
            </div>
            <div id="monowe-map-body"></div>
        `;
        document.body.appendChild(container);

        const style = document.createElement('style');
        style.textContent = `
            #monowe-minimap {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 999998;
                width: ${sz.w}px;
                background: ${c.bg};
                border-radius: 12px;
                overflow: hidden;
                box-shadow: ${c.shadow};
                font-family: 'Segoe UI', system-ui, sans-serif;
                backdrop-filter: blur(12px);
                cursor: move;
                user-select: none;
            }
            .monowe-map-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 12px;
                background: ${c.headerBg};
                color: ${c.text};
                font-size: 0.8rem;
                letter-spacing: 0.05em;
            }
            .monowe-header-left {
                display: flex;
                align-items: baseline;
                gap: 6px;
                min-width: 0;
                flex: 1;
            }
            .monowe-header-right {
                display: flex;
                align-items: center;
                gap: 2px;
                flex-shrink: 0;
            }
            .monowe-loc-label {
                color: ${c.textDim};
                font-size: 0.75rem;
                white-space: nowrap;
            }
            .monowe-loc-name {
                color: ${c.locName};
                font-size: 0.78rem;
                font-weight: 600;
                max-width: 140px;
                display: inline-block;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                vertical-align: bottom;
            }
            .monowe-map-header button {
                background: none;
                border: none;
                color: ${c.textDim};
                cursor: pointer;
                font-size: 1.1rem;
                padding: 0 4px;
                line-height: 1;
            }
            .monowe-map-header button:hover {
                color: ${c.btnHover};
            }
            #monowe-map-body {
                height: ${sz.h}px;
                transition: height 0.3s ease;
            }
            #monowe-map-body.collapsed {
                height: 0;
            }
            #monowe-map-body .leaflet-container {
                width: 100%;
                height: 100%;
                background: #1a1a2e;
            }
            #monowe-map-body .leaflet-control-zoom a {
                background: rgba(18,18,18,0.9) !important;
                color: #00d4ff !important;
                border: 1px solid rgba(0,212,255,0.3) !important;
                width: 28px !important;
                height: 28px !important;
                line-height: 28px !important;
                font-size: 16px !important;
            }
            #monowe-map-body .leaflet-control-zoom a:hover {
                background: rgba(0,212,255,0.2) !important;
                color: #fff !important;
            }
        `;
        document.head.appendChild(style);

        const toggleBtn = document.getElementById('monowe-map-toggle');
        const settingsBtn = document.getElementById('monowe-settings-btn');
        const mapBody = document.getElementById('monowe-map-body');
        toggleBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            const collapsed = mapBody.classList.toggle('collapsed');
            toggleBtn.textContent = collapsed ? '+' : '\u2212';
            if (!collapsed) applySize();
        });
        settingsBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            toggleSettingsPanel();
        });

        let isDragging = false, offsetX, offsetY;
        container.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON') return;
            isDragging = true;
            offsetX = e.clientX - container.getBoundingClientRect().left;
            offsetY = e.clientY - container.getBoundingClientRect().top;
            container.style.transition = 'none';
        });
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            container.style.left = (e.clientX - offsetX) + 'px';
            container.style.top = (e.clientY - offsetY) + 'px';
            container.style.right = 'auto';
            container.style.bottom = 'auto';
        });
        document.addEventListener('mouseup', () => {
            isDragging = false;
            container.style.transition = '';
        });

        return container;
    }

    // ─── MAP INITIALIZATION ───────────────────────────────────────
    function waitForLeaflet() {
        return new Promise((resolve) => {
            const check = setInterval(() => {
                if (window.L) { clearInterval(check); resolve(); }
            }, 200);
        });
    }

    async function initLeafletMap() {
        await waitForLeaflet();
        const map = L.map('monowe-map-body', {
            zoomControl: true,
            attributionControl: false,
        }).setView([20, 0], 5);

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '&copy; OpenStreetMap',
        }).addTo(map);

        const marker = L.circleMarker([0, 0], {
            radius: 8,
            color: '#00d4ff',
            fillColor: '#00d4ff',
            fillOpacity: 0.8,
            weight: 2,
        }).addTo(map);

        return { map, marker };
    }

    function updateMap(mapObj, coords) {
        if (!mapObj || !coords) return;
        const { map, marker } = mapObj;
        const latlng = [coords.lat, coords.lng];
        marker.setLatLng(latlng);
        map.setView(latlng, 12, { animate: true, duration: 0.8 });
    }


    // --- REVERSE GEOCODING ---
    let geocodeTimer = null;
    let lastGeocoded = null;

    async function reverseGeocode(lat, lng) {
        const key = lat.toFixed(3) + ',' + lng.toFixed(3);
        if (key === lastGeocoded) return;
        lastGeocoded = key;

        try {
            const resp = await fetch(
                'https://nominatim.openstreetmap.org/reverse?format=json&lat=' + lat + '&lon=' + lng + '&zoom=10&accept-language=en',
                { headers: { 'User-Agent': 'monowe-openguessr/1.2' } }
            );
            const data = await resp.json();
            const el = document.getElementById('monowe-location-name');
            if (!el) return;

            const addr = data.address || {};
            const city = addr.city || addr.town || addr.village || addr.hamlet || addr.municipality || '';
            const country = addr.country || '';
            const region = addr.state || addr.region || '';

            let label = city || region || country || (data.display_name || '').split(',').slice(0, 2).join(', ') || '...';
            if (city && country && city !== country) label = city + ', ' + country;
            else if (region && country && region !== country) label = region + ', ' + country;

            el.textContent = label;
            el.title = data.display_name || '';
        } catch (e) {
            console.log('[monowe] geocode error:', e);
        }
    }

    // ─── MAIN ─────────────────────────────────────────────────────
    let mapObj = null;

    async function main() {
        console.log('[monowe] script starting...');

        // Register listener immediately — stores coords until map is ready
        listeners.push((coords) => {
            if (mapObj) {
                updateMap(mapObj, coords);
            }
            // Reverse geocode location name
            clearTimeout(geocodeTimer);
            geocodeTimer = setTimeout(() => reverseGeocode(coords.lat, coords.lng), 300);
            // If map not ready yet, coords are in lastCoords — will be applied below
        });

        // Start all hooks immediately
        hookNetwork();
        hookJSONP();
        hookIframes();
        hookGoogleMapsAPI();

        // Phase 1: Welcome animation
        await showWelcomeAnimation();

        // Phase 2: Create minimap
        createMiniMap();
        mapObj = await initLeafletMap();
        console.log('[monowe] minimap ready');

        // Apply any coords that arrived before map was ready
        if (lastCoords) {
            console.log('[monowe] applying early coords:', lastCoords.lat, lastCoords.lng);
            updateMap(mapObj, lastCoords);
            reverseGeocode(lastCoords.lat, lastCoords.lng);
        }

        // Scan page HTML for embedded coordinates
        scanPageHTML();
        setTimeout(scanPageHTML, 3000);
        setTimeout(scanPageHTML, 6000);
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        main();
    } else {
        document.addEventListener('DOMContentLoaded', main);
    }
})();