Pinpointing Live Challenges

The script that makes pinpointing matter, even in Live Challenges.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Pinpointing Live Challenges
// @namespace    http://tampermonkey.net/
// @version      0.55
// @description  The script that makes pinpointing matter, even in Live Challenges.
// @match        https://www.geoguessr.com/*
// @icon         https://i.imgur.com/eKp3nIa.png
// @grant        GM_xmlhttpRequest
// @connect      firebasedatabase.app
// @require      https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js
// @require      https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) return;

    // --- SETTINGS ---
    const PANEL_DELAY = 4000;
    const MAX_NOTIFICATIONS = 2;
    const BACKUP_POLLING_RATE_MS = 5000;

    // --- CONFIGURATION ---
    const firebaseConfig = {
        apiKey: "AIzaSyCmkGRSezro0Sl27qT4vXetnA7kKJRdYog",
        authDomain: "pinpointinglivechallenges.firebaseapp.com",
        databaseURL: "https://pinpointinglivechallenges-default-rtdb.europe-west1.firebasedatabase.app",
        projectId: "pinpointinglivechallenges",
        storageBucket: "pinpointinglivechallenges.firebasestorage.app",
        messagingSenderId: "707336313368",
        appId: "1:707336313368:web:791e7fa09a22dc7596416d",
        measurementId: "G-GWZ6ZXJ97X"
    };

    try {
        if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);
    } catch (e) { console.error(e); }
    const db = firebase.database();

    // Global State
    let currentPollingId = null;
    let gamePollInterval = null;
    let firebaseBackupInterval = null;
    let dbListenerRef = null;
    let connectedListenerRef = null; // NEW: Monitor connection status
    let lastReportedRound = 0;
    let podiumObserver = null;
    let celebratedPlayers = new Set();
    let panelTimeout = null;
    let prevRoundFinished = false;

    // Smart Connection State
    let isSocketHealthy = false;

    // Identity Cache
    let myLocalName = localStorage.getItem('pp_cached_name') || null;
    let myLocalId = localStorage.getItem('pp_cached_id') || null;

    // Data Cache
    let cachedGameData = {};
    let uiState = {
        currentRoundNum: 1,
        totalRounds: 10,
        isRoundFinished: false,
        isTimeUp: false,
        isGracePeriod: false,
        isGameFinished: false,
        podiumDetected: false,
        roundEndTime: 0,
        roundTimeLimit: 0
    };

    // --- ZOMBIE-PROOF SAVER ---
    function safeSave(path, payload) {
        return new Promise((resolve) => {
            let isResolved = false;
            // 1. Try Standard SDK
            db.ref(path).set(payload).then(() => {
                if (!isResolved) { isResolved = true; resolve(); }
            }).catch(() => {});
            // 2. Watchdog / Fallback
            setTimeout(() => {
                if (!isResolved) {
                    console.warn("[Pinpointing] Write hung. Sending via GM_REST...");
                    const restUrl = `${firebaseConfig.databaseURL}/${path}.json`;
                    GM_xmlhttpRequest({
                        method: "PUT",
                        url: restUrl,
                        data: JSON.stringify(payload),
                        headers: { "Content-Type": "application/json" },
                        onload: function() {
                            if (!isResolved) { isResolved = true; console.log("[Pinpointing] GM_REST Write Success"); resolve(); }
                        }
                    });
                }
            }, 2000);
        });
    }

    // --- SMART HYBRID LISTENER ---
    function setupDatabaseListener(gameId) {
        if (dbListenerRef) dbListenerRef.off();
        console.log(`[Pinpointing] Listening to Firebase (Socket): ${gameId}`);

        dbListenerRef = db.ref(`games/${gameId}`);
        dbListenerRef.on('value', (snapshot) => {
            const val = snapshot.val();
            if (val) {
                cachedGameData = val;
                renderVisuals();
            }
        });

        // We listen to the special ".info/connected" node.
        // This is true if the websocket is open, false if blocked/disconnected.
        if (connectedListenerRef) connectedListenerRef.off();
        connectedListenerRef = db.ref(".info/connected");
        connectedListenerRef.on("value", (snap) => {
            isSocketHealthy = (snap.val() === true);
            // console.log("[Pinpointing] Socket Status:", isSocketHealthy ? "CONNECTED" : "DISCONNECTED");
        });

        if (firebaseBackupInterval) clearInterval(firebaseBackupInterval);
        firebaseBackupInterval = setInterval(() => {
            // ONLY poll if Firebase itself says "I am disconnected"
            // This protects your quota during quiet rounds where the socket is open but no data is moving.
            if (!isSocketHealthy) {
                console.log("[Pinpointing] Socket disconnected. Fetching via GM_REST...");
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `${firebaseConfig.databaseURL}/games/${gameId}.json`,
                    onload: (response) => {
                        if (response.status === 200 && response.responseText) {
                            try {
                                const val = JSON.parse(response.responseText);
                                if (val) {
                                    cachedGameData = val;
                                    renderVisuals();
                                }
                            } catch (e) { console.error("[Pinpointing] JSON Parse Error", e); }
                        }
                    }
                });
            }
        }, BACKUP_POLLING_RATE_MS);
    }

    // --- HELPER: ID PARSER ---
    function getGameIdFromUrl() {
        const path = location.pathname;
        const match = path.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
        return match ? match[0] : null;
    }

    // --- HELPER: GET IDENTITY ---
    function getIdentityFallback() {
        if (myLocalId && myLocalName) return { id: myLocalId, nick: myLocalName };
        try {
            const el = document.getElementById("__NEXT_DATA__");
            if (el) {
                const data = JSON.parse(el.innerText);
                const user = data.props?.pageProps?.account ||
                             data.props?.pageProps?.user ||
                             data.props?.accountProps?.account?.user;
                if (user) {
                    const foundId = user.id || user.userId;
                    if (foundId) {
                        saveIdentity(foundId, user.nick);
                        return { id: foundId, nick: user.nick };
                    }
                }
            }
        } catch (e) { console.warn("[Pinpointing] Identity fallback error:", e); }
        return null;
    }

    function saveIdentity(id, name) {
        if (!id) return;
        myLocalId = id;
        myLocalName = name;
        localStorage.setItem('pp_cached_id', id);
        localStorage.setItem('pp_cached_name', name);
    }

    // --- UI STYLES ---
    const STYLES = `
        #pp-results-panel {
            position: absolute; bottom: 2%; left: 50%; transform: translateX(-50%);
            width: 750px; background: rgba(23, 23, 23, 0.98);
            border: 3px solid #594eaf; border-radius: 12px;
            color: white; z-index: 4; font-family: 'ggFont', sans-serif;
            box-shadow: 0 10px 40px rgba(0,0,0,0.8);
            display: none; flex-direction: column; overflow: hidden; transition: top 0.5s ease;
        }
        #pp-results-panel.final-mode { top: 15% !important; }
        #pp-results-panel.visible { display: flex; }
        .pp-tabs { display: flex; border-bottom: 2px solid #555; cursor: pointer; background: #111; }
        .pp-tab { flex: 1; padding: 15px; text-align: center; font-size: 16px; font-weight: bold; color: #888; text-transform: uppercase; transition: all 0.2s; }
        .pp-tab:hover { background: #222; color: #aaa; }
        .pp-tab.active { background: linear-gradient(180deg,#322a6a 0%, #594eaf 100%); color: white; border-bottom: 4px solid #8f86e6; }
        .pp-tab-content { display: none; min-height: 150px; max-height: 35vh; overflow-y: auto; }
        #pp-results-panel.final-mode .pp-tab-content { max-height: 60vh; }
        .pp-tab-content.active { display: block; }
        .pp-tab-content::-webkit-scrollbar { width: 8px; }
        .pp-tab-content::-webkit-scrollbar-track { background: #1a1a1a; }
        .pp-tab-content::-webkit-scrollbar-thumb { background: #594eaf; border-radius: 4px; }
        .pp-columns { display: grid; padding: 10px 20px; border-bottom: 1px solid #444; color: #aaa; font-size: 12px; text-transform: uppercase; font-weight: bold; background: #1a1a1a; position: sticky; top: 0; z-index: 10; }
        .cols-round { grid-template-columns: 50px 1fr 100px 80px 80px; }
        .cols-total { grid-template-columns: 50px 1fr 100px; }
        .cols-final { grid-template-columns: 50px 1fr 100px 100px 80px 100px; }
        .pp-row { display: flex; align-items: center; padding: 10px 20px; border-bottom: 1px solid #333; font-size: 15px; }
        .pp-row:nth-child(even) { background: rgba(255,255,255,0.03); }
        .pp-row.is-me { background: rgba(90, 219, 149, 0.15); border-left: 4px solid #5adb95; }
        .grid-round { display: grid; grid-template-columns: 50px 1fr 100px 80px 80px; align-items: center; }
        .grid-total { display: grid; grid-template-columns: 50px 1fr 100px; align-items: center; }
        .grid-final { display: grid; grid-template-columns: 50px 1fr 100px 100px 80px 100px; align-items: center; }
        .pp-row.top-3 { font-size: 18px; padding: 15px 20px; background: rgba(255, 215, 0, 0.05); }
        .pp-row.rank-1 { border-left: 4px solid #FFD700; }
        .pp-row.rank-2 { border-left: 4px solid #C0C0C0; }
        .pp-row.rank-3 { border-left: 4px solid #CD7F32; }
        .pp-rank { font-weight: bold; color: #888; }
        .pp-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
        .pp-score { color: #fff; }
        .pp-time { color: #ccc; font-size: 13px; }
        .pp-dist { color: #ccc; font-size: 13px; }
        .pp-5k { color: #5adb95; font-weight: bold; }
        .pp-gained { color: #5adb95; font-weight: bold; text-align: right; }
        .pp-total-pts { color: #ffcc00; font-weight: bold; font-size: 16px; text-align: right; }
        #pp-notifs { position: fixed; top: 180px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 10px; z-index: 100000; }
        .pp-toast { background: linear-gradient(180deg,#322a6a 0%, #594eaf 100%); border: 2px solid #8f86e6; color: white; padding: 15px 30px; border-radius: 50px; font-family: 'ggFont', sans-serif; font-size: 20px; font-weight: bold; box-shadow: 0 5px 20px rgba(0,0,0,0.5); animation: pp-slide-in 0.3s ease-out forwards; opacity: 0; }
        .pp-toast.perfect { background: linear-gradient(180deg, #2c6e49 0%, #479e65 100%); border-color: #5adb95; }
        .pp-toast.close { background: linear-gradient(180deg, #8a6d0b 0%, #b59218 100%); border-color: #ffd700; }
        @keyframes pp-slide-in { 0% { transform: translateY(-20px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }
        @keyframes pp-fade-out { 0% { opacity: 1; } 100% { opacity: 0; } }
        div[class*="multiplayer-round-results_leaderboard"] { display: none !important; }
        div[class*="leaderboard_leaderboard"] { display: none !important; }
        div[class*="podium_podiumWrapper"], div[class*="styles_podiumWrapper"] { display: none !important; }
        div[class*="avatar-podium_root"] { display: none !important; }
        div[class*="multiplayer-round-results_playerActionSection"] { margin-top: .5rem !important; }
        div[class*="score_score"] { margin: .5rem 0 .5rem !important; }
        div[class*="multiplayer-round-results_content"] { padding-top: 1.5rem }
        div[class*="multiplayer-round-results_waitingForHostMessage"] { display: none !important; }
    `;

    function createUI() {
        if (document.getElementById('pp-results-panel')) return;
        const styleSheet = document.createElement("style");
        styleSheet.innerText = STYLES;
        document.head.appendChild(styleSheet);
        const panel = document.createElement('div');
        panel.id = 'pp-results-panel';
        panel.innerHTML = `
            <div class="pp-tabs" id="pp-tabs-header">
                <div id="tab-btn-round" class="pp-tab active">Round Results</div>
                <div id="tab-btn-total" class="pp-tab">Total Standings</div>
            </div>
            <div id="view-round" class="pp-tab-content active"><div class="pp-columns cols-round"><div>#</div><div>Player</div><div>Score</div><div>Time</div><div>+Pts</div></div><div id="list-round"></div></div>
            <div id="view-total" class="pp-tab-content"><div class="pp-columns cols-total"><div>#</div><div>Player</div><div style="text-align:right">Total Pts</div></div><div id="list-total"></div></div>
            <div id="view-final" class="pp-tab-content"><div class="pp-columns cols-final"><div>#</div><div>Player</div><div>Time</div><div>Dist</div><div>5Ks</div><div style="text-align:right">Pts</div></div><div id="list-final"></div></div>
        `;
        document.body.appendChild(panel);
        document.getElementById('tab-btn-round').onclick = () => switchTab('round');
        document.getElementById('tab-btn-total').onclick = () => switchTab('total');
        const notifs = document.createElement('div');
        notifs.id = 'pp-notifs';
        document.body.appendChild(notifs);
    }

    function switchTab(tabName) {
        document.getElementById('tab-btn-round').classList.toggle('active', tabName === 'round');
        document.getElementById('tab-btn-total').classList.toggle('active', tabName === 'total');
        document.getElementById('view-round').classList.toggle('active', tabName === 'round');
        document.getElementById('view-total').classList.toggle('active', tabName === 'total');
    }

    function showNotification(message, type = 'normal') {
        const container = document.getElementById('pp-notifs');
        if (!container) return;
        while (container.childElementCount >= MAX_NOTIFICATIONS) container.firstElementChild.remove();
        const toast = document.createElement('div');
        toast.className = `pp-toast ${type}`;
        toast.innerText = message;
        container.appendChild(toast);
        setTimeout(() => {
            toast.style.animation = 'pp-fade-out 0.5s ease forwards';
            setTimeout(() => toast.remove(), 500);
        }, 3000);
    }

    function renderVisuals() {
        if (!cachedGameData) return;
        let totalLeaderboard = {};
        let roundResults = [];
        const isPodiumTime = uiState.podiumDetected && uiState.isGameFinished;
        const targetRoundNum = uiState.isGracePeriod ? (uiState.currentRoundNum - 1) : uiState.currentRoundNum;
        let hasGuessedCurrent = false;
        if (myLocalId) {
            const rData = cachedGameData[`round_${uiState.currentRoundNum}`];
            if (rData && rData[myLocalId]) hasGuessedCurrent = true;
        }
        const showPanel = isPodiumTime || uiState.isRoundFinished || uiState.isGracePeriod || uiState.isTimeUp || hasGuessedCurrent;
        Object.keys(cachedGameData).forEach(roundKey => {
            if (!roundKey.startsWith("round_")) return;
            const rNum = parseInt(roundKey.replace("round_", ""));
            const playersInRound = Object.values(cachedGameData[roundKey]);
            if (rNum === uiState.currentRoundNum) {
                playersInRound.forEach(p => {
                    const uniqueKey = `${roundKey}_${p.name}`;
                    if (!celebratedPlayers.has(uniqueKey)) {
                        if (p.score === 5000) { showNotification(`${p.name} JUST 5K'D! (${p.time}s)`, 'perfect'); celebratedPlayers.add(uniqueKey); }
                        else if (p.score >= 4990 && p.score < 5000) { showNotification(`${p.name} was close! (${p.score})`, 'close'); celebratedPlayers.add(uniqueKey); }
                    }
                });
            }
            playersInRound.sort((a, b) => (b.score !== a.score) ? b.score - a.score : a.time - b.time);
            const totalPlayers = playersInRound.length;
            let previousP = null;
            let previousEarned = 0;
            playersInRound.forEach((player, index) => {
                const pName = player.name;
                let earned = (totalPlayers - 1) - index;
                if (previousP && player.score === previousP.score && player.time === previousP.time) earned = previousEarned;
                if (!totalLeaderboard[pName]) totalLeaderboard[pName] = { name: pName, totalPinPoints: 0, num5ks: 0, totalTime: 0, totalDist: 0 };
                totalLeaderboard[pName].totalPinPoints += earned;
                totalLeaderboard[pName].totalTime += (player.time || 0);
                totalLeaderboard[pName].totalDist += (player.distance || 0);
                if (player.score === 5000) totalLeaderboard[pName].num5ks++;
                if (rNum === targetRoundNum) player.pointsEarned = earned;
                previousP = player;
                previousEarned = earned;
            });
            if (rNum === targetRoundNum) roundResults = playersInRound;
        });

        const panel = document.getElementById('pp-results-panel');
        if (panel) showPanel ? panel.classList.add('visible') : panel.classList.remove('visible');

        if (isPodiumTime) {
            if (Object.keys(totalLeaderboard).length === 0) { if (panel) panel.classList.remove('visible'); return; }
            if (panel) panel.classList.add('final-mode');
            const tabsHeader = document.getElementById('pp-tabs-header');
            if (tabsHeader) tabsHeader.style.display = 'none';
            document.getElementById('view-round').classList.remove('active');
            document.getElementById('view-total').classList.remove('active');
            document.getElementById('view-final').style.display = 'block';
            const listFinal = document.getElementById('list-final');
            if (listFinal) {
                const finalRankings = Object.values(totalLeaderboard).sort((a, b) => b.totalPinPoints !== a.totalPinPoints ? b.totalPinPoints - a.totalPinPoints : (b.num5ks !== a.num5ks ? b.num5ks - a.num5ks : a.totalDist - b.totalDist));
                let html = '<div style="padding:15px; text-align:center; font-size:24px; font-weight:bold; color:#ffd700;">FINAL STANDINGS</div>';
                finalRankings.forEach((p, index) => {
                    const isMe = (p.name === myLocalName);
                    const rank = index + 1;
                    let medal = '', rowExtraClass = '';
                    if (rank === 1) { medal = '🥇 '; rowExtraClass = 'top-3 rank-1'; }
                    else if (rank === 2) { medal = '🥈 '; rowExtraClass = 'top-3 rank-2'; }
                    else if (rank === 3) { medal = '🥉 '; rowExtraClass = 'top-3 rank-3'; }
                    let distStr = Math.round(p.totalDist) + "m";
                    if (p.totalDist > 5000) distStr = (p.totalDist / 1000).toFixed(1) + "km";
                    html += `<div class="pp-row grid-final ${isMe ? 'is-me' : ''} ${rowExtraClass}"><div class="pp-rank">${medal || '#' + rank}</div><div class="pp-name">${p.name}</div><div class="pp-time">${p.totalTime.toFixed(0)}s</div><div class="pp-dist">${distStr}</div><div class="pp-5k">${p.num5ks}</div><div class="pp-total-pts">${p.totalPinPoints}</div></div>`;
                });
                listFinal.innerHTML = html;
            }
            return;
        }

        if (showPanel) {
            if (panel) panel.classList.remove('final-mode');
            const tabsHeader = document.getElementById('pp-tabs-header');
            if (tabsHeader) tabsHeader.style.display = 'flex';
            document.getElementById('view-final').style.display = 'none';
            const isFinalRound = (targetRoundNum === uiState.totalRounds);
            const tabTotal = document.getElementById('tab-btn-total');
            if (tabTotal) {
                if (isFinalRound) { tabTotal.style.display = 'none'; switchTab('round'); }
                else { tabTotal.style.display = 'block'; }
            }
            if (!document.getElementById('view-round').classList.contains('active') && !document.getElementById('view-total').classList.contains('active')) switchTab('round');
            const listRound = document.getElementById('list-round');
            if (listRound) {
                let html = '';
                roundResults.forEach((p, index) => {
                    const isMe = (p.name === myLocalName);
                    html += `<div class="pp-row grid-round ${isMe ? 'is-me' : ''}"><div class="pp-rank">#${index + 1}</div><div class="pp-name">${p.name}</div><div class="pp-score">${p.score}</div><div class="pp-time">${p.time}s</div><div class="pp-gained">+${p.pointsEarned !== undefined ? p.pointsEarned : '?'}</div></div>`;
                });
                listRound.innerHTML = html || '<div style="padding:20px;text-align:center;color:#666;">No guesses recorded.</div>';
            }
            const listTotal = document.getElementById('list-total');
            if (listTotal) {
                const totalRankings = Object.values(totalLeaderboard).sort((a, b) => b.totalPinPoints !== a.totalPinPoints ? b.totalPinPoints - a.totalPinPoints : (b.num5ks !== a.num5ks ? b.num5ks - a.num5ks : a.totalDist - b.totalDist));
                let html = '';
                totalRankings.forEach((p, index) => {
                    const isMe = (p.name === myLocalName);
                    html += `<div class="pp-row grid-total ${isMe ? 'is-me' : ''}"><div class="pp-rank">#${index + 1}</div><div class="pp-name">${p.name}</div><div class="pp-total-pts">${p.totalPinPoints}</div></div>`;
                });
                listTotal.innerHTML = html;
            }
        }
    }

    function startPodiumObserver() {
        if (podiumObserver) return;
        podiumObserver = new MutationObserver((mutations) => {
            const pageText = document.body.innerText;
            const isSummaryScreen = pageText.includes("Game breakdown") || pageText.includes("Game summary");
            const avatarPodium = document.querySelector('div[class*="avatar-podium_root"]');
            if ((isSummaryScreen || avatarPodium) && uiState.isGameFinished) {
                if (!uiState.podiumDetected) { uiState.podiumDetected = true; renderVisuals(); }
            }
        });
        podiumObserver.observe(document.body, { childList: true, subtree: true });
    }

    // --- WATCHDOG ---
    function startWatchdog(gameId, myId, myName) {
        setInterval(() => {
            if (!uiState.roundEndTime || !myId) return;
            const now = Date.now();
            if (now > (uiState.roundEndTime + 5000)) {
                if (lastReportedRound < uiState.currentRoundNum) {
                    const payload = { name: myName, score: 0, time: uiState.roundTimeLimit, distance: 20000000, updatedAt: firebase.database.ServerValue.TIMESTAMP };
                    safeSave(`games/${gameId}/round_${uiState.currentRoundNum}/${myId}`, payload).then(() => {
                        lastReportedRound = uiState.currentRoundNum;
                        renderVisuals();
                    });
                }
                if (!uiState.isTimeUp) { uiState.isTimeUp = true; renderVisuals(); }
            }
        }, 1000);
    }

    async function fetchChallengeData(gameId) {
        try {
            const res = await fetch(`https://game-server.geoguessr.com/api/live-challenge/${gameId}`, { method: "GET", credentials: "include" });
            if (res.ok) processGameData(await res.json());
        } catch (e) { console.error("[Pinpointing] Poll Error:", e); }
    }

    function processGameData(data) {
        let myId = data.playerPositions?.gameEntry?.id;
        let myName = data.playerPositions?.gameEntry?.name;
        if (myId) { saveIdentity(myId, myName); }
        else { const fallback = getIdentityFallback(); if (fallback) { myId = fallback.id; myName = fallback.nick; } }

        const currentRound = data.currentRoundNumber;
        const totalRounds = data.roundCount || data.options?.roundCount || data.totalSteps || 10;

        if (myId) {
            if (!window.ppWatchdogStarted) { startWatchdog(data.gameId, myId, myName); window.ppWatchdogStarted = true; }
            const roundStartCheck = lastReportedRound + 1;
            const roundEndCheck = currentRound;
            for (let r = roundStartCheck; r <= roundEndCheck; r++) {
                const currentGuess = data.guesses.find(g => g.roundNumber === r);
                const roundObj = data.rounds.find(ro => ro.roundNumber === r);
                const isRoundOver = roundObj && roundObj.state === "Ended";
                const roundTimeLimit = roundObj?.roundTime || data.options?.roundTime || 9999;
                const roundStartTime = roundObj ? new Date(roundObj.startTime).getTime() : 0;
                const timeNow = Date.now();
                const timeDeadline = roundStartTime + (roundTimeLimit * 1000) + 5000;
                const isTimeUp = (roundStartTime > 0) && (timeNow > timeDeadline);

                let payload = null;
                if (currentGuess) { payload = { name: myName, score: currentGuess.score, time: currentGuess.time, distance: currentGuess.distance, updatedAt: firebase.database.ServerValue.TIMESTAMP }; }
                else if (isRoundOver || isTimeUp) { payload = { name: myName, score: 0, time: roundTimeLimit, distance: 20000000, updatedAt: firebase.database.ServerValue.TIMESTAMP }; }
                else { break; }

                if (payload) {
                    safeSave(`games/${data.gameId}/round_${r}/${myId}`, payload);
                    lastReportedRound = r;
                }
            }
        }
        uiState.currentRoundNum = currentRound;
        uiState.totalRounds = totalRounds;
        const currentRoundObj = data.rounds.find(r => r.roundNumber === currentRound);
        const isRoundOver = currentRoundObj && currentRoundObj.state === "Ended";
        uiState.isRoundFinished = isRoundOver;
        uiState.isGameFinished = (data.status === "Finished" || data.status === "Ended");
        const curRoundStartTime = currentRoundObj ? new Date(currentRoundObj.startTime).getTime() : 0;
        const curRoundLimit = currentRoundObj?.roundTime || data.options?.roundTime || 9999;
        const curTimeNow = Date.now();
        const curDeadline = curRoundStartTime + (curRoundLimit * 1000) + 5000;
        if (curRoundStartTime > 0) {
            uiState.isTimeUp = (curTimeNow > curDeadline);
            uiState.roundEndTime = curRoundStartTime + (curRoundLimit * 1000);
            uiState.roundTimeLimit = curRoundLimit;
        } else { uiState.isTimeUp = false; }
        if (prevRoundFinished && !isRoundOver) {
             if (!uiState.isGracePeriod && !panelTimeout && lastReportedRound > 0) {
                 uiState.isGracePeriod = true;
                 panelTimeout = setTimeout(() => { uiState.isGracePeriod = false; panelTimeout = null; renderVisuals(); }, PANEL_DELAY);
             }
        }
        if (isRoundOver) { uiState.isGracePeriod = false; if (panelTimeout) { clearTimeout(panelTimeout); panelTimeout = null; } }
        prevRoundFinished = isRoundOver;

        // Ensure Listener is Active
        if (currentPollingId !== data.gameId) {
             setupDatabaseListener(data.gameId);
             currentPollingId = data.gameId;
        }

        renderVisuals();
    }

    function startPolling(gameId) {
        if (currentPollingId === gameId) return;
        currentPollingId = gameId;
        lastReportedRound = 0;
        celebratedPlayers.clear();
        cachedGameData = {};
        window.ppWatchdogStarted = false;
        prevRoundFinished = false;
        uiState.isGameFinished = false;
        uiState.podiumDetected = false;
        uiState.isGracePeriod = false;
        isSocketHealthy = false; // Reset

        const old = document.getElementById('pp-results-panel');
        if (old) old.remove();
        createUI();
        startPodiumObserver();

        setupDatabaseListener(gameId);

        if (!myLocalId) { const fallback = getIdentityFallback(); if (fallback) { myLocalId = fallback.id; myLocalName = fallback.nick; } }
        fetchChallengeData(gameId);
        if (gamePollInterval) clearInterval(gamePollInterval);
        gamePollInterval = setInterval(() => fetchChallengeData(gameId), 2000);
    }

    function stopPolling() {
        if (!currentPollingId) return;
        currentPollingId = null;
        if (gamePollInterval) clearInterval(gamePollInterval);
        if (firebaseBackupInterval) clearInterval(firebaseBackupInterval);
        if (podiumObserver) { podiumObserver.disconnect(); podiumObserver = null; }
        const panel = document.getElementById('pp-results-panel');
        if (panel) panel.remove();
        const notifs = document.getElementById('pp-notifs');
        if (notifs) notifs.remove();
        if (dbListenerRef) { dbListenerRef.off(); dbListenerRef = null; }
        if (connectedListenerRef) { connectedListenerRef.off(); connectedListenerRef = null; }
    }

    function heartbeat() {
        if (location.pathname.includes("/live-challenge/")) {
            const gameId = getGameIdFromUrl();
            if (gameId) { if (currentPollingId !== gameId) startPolling(gameId); }
        } else { if (currentPollingId) stopPolling(); }
    }

    setInterval(heartbeat, 1000);
    heartbeat();

})();