Pinpointing Live Challenges

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

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

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

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

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

이 스크립트를 설치하려면 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 = 5500;

    // --- 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();

})();