Country Streak Counter

Adds a country streak counter to the GeoGuessr website

当前为 2022-10-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Country Streak Counter
// @version      1.3.5
// @description  Adds a country streak counter to the GeoGuessr website
// @match        https://www.geoguessr.com/*
// @author       victheturtle#5159
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// @namespace    https://greasyfork.org/users/967692-victheturtle
// ==/UserScript==
// Credits to subsymmetry for the original version of the Streak Counter

const ENABLED_ON_CHALLENGES = false;
//                            ^^^^^ Replace with true or false to enable the counter on challenges as well

const API_Key = 'INSERT_BIGDATACLOUD_API_KEY_HERE';
//               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Replace INSERT_BIGDATACLOUD_API_KEY_HERE with your API key (keep the quote marks)

const AUTOMATIC = true;
//                ^^^^ Replace with false for a manual counter. Note that without a valid API key, the counter will be manual anyway

const CountryDict = {
    AF: 'AF',
    AX: 'FI', // Aland Islands
    AL: 'AL',
    DZ: 'DZ',
    AS: 'US', // American Samoa
    AD: 'AD',
    AO: 'AO',
    AI: 'GB', // Anguilla
    AQ: 'AQ', // Antarctica
    AG: 'AG',
    AR: 'AR',
    AM: 'AM',
    AW: 'NL', // Aruba
    AU: 'AU',
    AT: 'AT',
    AZ: 'AZ',
    BS: 'BS',
    BH: 'BH',
    BD: 'BD',
    BB: 'BB',
    BY: 'BY',
    BE: 'BE',
    BZ: 'BZ',
    BJ: 'BJ',
    BM: 'GB', // Bermuda
    BT: 'BT',
    BO: 'BO',
    BQ: 'NL', // Bonaire, Sint Eustatius, Saba
    BA: 'BA',
    BW: 'BW',
    BV: 'NO', // Bouvet Island
    BR: 'BR',
    IO: 'GB', // British Indian Ocean Territory
    BN: 'BN',
    BG: 'BG',
    BF: 'BF',
    BI: 'BI',
    KH: 'KH',
    CM: 'CM',
    CA: 'CA',
    CV: 'CV',
    KY: 'UK', // Cayman Islands
    CF: 'CF',
    TD: 'TD',
    CL: 'CL',
    CN: 'CN',
    CX: 'AU', // Christmas Islands
    CC: 'AU', // Cocos (Keeling) Islands
    CO: 'CO',
    KM: 'KM',
    CG: 'CG',
    CD: 'CD',
    CK: 'NZ', // Cook Islands
    CR: 'CR',
    CI: 'CI',
    HR: 'HR',
    CU: 'CU',
    CW: 'NL', // Curacao
    CY: 'CY',
    CZ: 'CZ',
    DK: 'DK',
    DJ: 'DJ',
    DM: 'DM',
    DO: 'DO',
    EC: 'EC',
    EG: 'EG',
    SV: 'SV',
    GQ: 'GQ',
    ER: 'ER',
    EE: 'EE',
    ET: 'ET',
    FK: 'GB', // Falkland Islands
    FO: 'DK', // Faroe Islands
    FJ: 'FJ',
    FI: 'FI',
    FR: 'FR',
    GF: 'FR', // French Guiana
    PF: 'FR', // French Polynesia
    TF: 'FR', // French Southern Territories
    GA: 'GA',
    GM: 'GM',
    GE: 'GE',
    DE: 'DE',
    GH: 'GH',
    GI: 'UK', // Gibraltar
    GR: 'GR',
    GL: 'DK', // Greenland
    GD: 'GD',
    GP: 'FR', // Guadeloupe
    GU: 'US', // Guam
    GT: 'GT',
    GG: 'GB', // Guernsey
    GN: 'GN',
    GW: 'GW',
    GY: 'GY',
    HT: 'HT',
    HM: 'AU', // Heard Island and McDonald Islands
    VA: 'VA',
    HN: 'HN',
    HK: 'CN', // Hong Kong
    HU: 'HU',
    IS: 'IS',
    IN: 'IN',
    ID: 'ID',
    IR: 'IR',
    IQ: 'IQ',
    IE: 'IE',
    IM: 'GB', // Isle of Man
    IL: 'IL',
    IT: 'IT',
    JM: 'JM',
    JP: 'JP',
    JE: 'GB', // Jersey
    JO: 'JO',
    KZ: 'KZ',
    KE: 'KE',
    KI: 'KI',
    KR: 'KR',
    KW: 'KW',
    KG: 'KG',
    LA: 'LA',
    LV: 'LV',
    LB: 'LB',
    LS: 'LS',
    LR: 'LR',
    LY: 'LY',
    LI: 'LI',
    LT: 'LT',
    LU: 'LU',
    MO: 'CN', // Macao
    MK: 'MK',
    MG: 'MG',
    MW: 'MW',
    MY: 'MY',
    MV: 'MV',
    ML: 'ML',
    MT: 'MT',
    MH: 'MH',
    MQ: 'FR', // Martinique
    MR: 'MR',
    MU: 'MU',
    YT: 'FR', // Mayotte
    MX: 'MX',
    FM: 'FM',
    MD: 'MD',
    MC: 'MC',
    MN: 'MN',
    ME: 'ME',
    MS: 'GB', // Montserrat
    MA: 'MA',
    MZ: 'MZ',
    MM: 'MM',
    NA: 'NA',
    NR: 'NR',
    NP: 'NP',
    NL: 'NL',
    AN: 'NL', // Netherlands Antilles
    NC: 'FR', // New Caledonia
    NZ: 'NZ',
    NI: 'NI',
    NE: 'NE',
    NG: 'NG',
    NU: 'NZ', // Niue
    NF: 'AU', // Norfolk Island
    MP: 'US', // Northern Mariana Islands
    NO: 'NO',
    OM: 'OM',
    PK: 'PK',
    PW: 'PW',
    PS: 'IL', // Palestine
    PA: 'PA',
    PG: 'PG',
    PY: 'PY',
    PE: 'PE',
    PH: 'PH',
    PN: 'GB', // Pitcairn
    PL: 'PL',
    PT: 'PT',
    PR: 'US', // Puerto Rico
    QA: 'QA',
    RE: 'FR', // Reunion
    RO: 'RO',
    RU: 'RU',
    RW: 'RW',
    BL: 'FR', // Saint Barthelemy
    SH: 'GB', // Saint Helena
    KN: 'KN',
    LC: 'LC',
    MF: 'FR', // Saint Martin
    PM: 'FR', // Saint Pierre and Miquelon
    VC: 'VC',
    WS: 'WS',
    SM: 'SM',
    ST: 'ST',
    SA: 'SA',
    SN: 'SN',
    RS: 'RS',
    SC: 'SC',
    SL: 'SL',
    SG: 'SG',
    SX: 'NL', // Sint Maarten
    SK: 'SK',
    SI: 'SI',
    SB: 'SB',
    SO: 'SO',
    ZA: 'ZA',
    GS: 'GB', // South Georgia and the South Sandwich Islands
    ES: 'ES',
    LK: 'LK',
    SD: 'SD',
    SR: 'SR',
    SJ: 'NO', // Svalbard and Jan Mayen
    SZ: 'SZ',
    SE: 'SE',
    CH: 'CH',
    SY: 'SY',
    TW: 'TW', // Taiwan
    TJ: 'TJ',
    TZ: 'TZ',
    TH: 'TH',
    TL: 'TL',
    TG: 'TG',
    TK: 'NZ', // Tokelau
    TO: 'TO',
    TT: 'TT',
    TN: 'TN',
    TR: 'TR',
    TM: 'TM',
    TC: 'GB', // Turcs and Caicos Islands
    TV: 'TV',
    UG: 'UG',
    UA: 'UA',
    AE: 'AE',
    GB: 'GB',
    US: 'US',
    UM: 'US', // US Minor Outlying Islands
    UY: 'UY',
    UZ: 'UZ',
    VU: 'VU',
    VE: 'VE',
    VN: 'VN',
    VG: 'GB', // British Virgin Islands
    VI: 'US', // US Virgin Islands
    WF: 'FR', // Wallis and Futuna
    EH: 'MA', // Western Sahara
    YE: 'YE',
    ZM: 'ZM',
    ZW: 'ZW'
};

const _AUTOMATIC = (AUTOMATIC && API_Key.toLowerCase().match("^(bdc_)?[a-f0-9]{32}$") != null);
const ERROR_RESP = -1000000;
let streak = parseInt(sessionStorage.getItem("Streak") || 0, 10);

const _cndic = {};
const hrefset = new Set();
async function scanStyles() {
    for (let node of document.querySelectorAll('head link[rel="preload"], head style[data-n-href*=".css"]')) {
        const href = node.href || location.origin+node.dataset.nHref;
        if (hrefset.has(href)) continue;
        hrefset.add(href);
        await fetch(href)
        .then(res => res.text())
        .then(stylesheet => {
            for (let className of stylesheet.split(".")) {
                const ind = className.indexOf("__");
                if (ind != -1) _cndic[className.substr(0, ind+2)] = className.substr(0, ind+7);
            };
        });
    };
};
const cn = (classNameStart) => _cndic[classNameStart]; // cn("status_section__") -> "status_section__8uP8o"

function checkGameMode() {
    return (location.pathname.startsWith("/game/") || (ENABLED_ON_CHALLENGES && location.pathname.startsWith("/challenge/")));
};

function geoguessrStyle(number) {
    return `<div class="${cn("guess-description-distance_distanceLabel__")}">
                <div class="${cn("slanted-wrapper_root__")} ${cn("slanted-wrapper_variantWhiteTransparent__")} ${cn("slanted-wrapper_roundnessSmall__")}">
                    <div class="${cn("slanted-wrapper_start__")} ${cn("slanted-wrapper_right__")}"></div>
                    <div class="${cn("guess-description-distance_distanceValue__")}">${number}</div>
                    <div class="${cn("slanted-wrapper_end__")} ${cn("slanted-wrapper_right__")}"></div>
                </div>
            </div>`;
};

function addStreakStatusBar() {
    const status_length = document.getElementsByClassName(cn("status_section__")).length;
    if (document.getElementById("country-streak") == null && status_length >= 3) {
        const newDiv = document.createElement("div");
        newDiv.className = cn('status_section__');
        newDiv.innerHTML = `<div class="${cn("status_label__")}">Streak</div>
        <div id="country-streak" class="${cn("status_value__")}">${streak}</div>`;
        const statusBar = document.getElementsByClassName(cn("status_inner__"))[0];
        statusBar.insertBefore(newDiv, statusBar.children[3]);
    };
};

function addStreakRoundResult() {
    if (document.getElementById("country-streak2") == null && !!document.querySelector('div[data-qa="guess-description"]')
            && !document.querySelector('div[class*="standard-final-result_section__"]')) {
        const newDiv = document.createElement("div");
        newDiv.innerHTML = `<div id="country-streak2" style="text-align:center;margin-top:10px;"><h2><i>Country Streak: ${streak}</i></h2></div>`;
        document.querySelector('div[data-qa="guess-description"]').appendChild(newDiv);
    };
};

function addStreakGameSummary() {
    if (document.getElementById("country-streak2") == null && !!document.querySelector('div[class*="standard-final-result_section__"]')) {
        const newDiv = document.createElement("div");
        newDiv.innerHTML = `<div id="country-streak2" style="text-align:center;margin-top:10px;"><h2><i>Country Streak: ${streak}</i></h2></div>`;
        const progressSection = document.querySelector('div[class*="standard-final-result_progressSection__"]');
        progressSection.parentNode.insertBefore(newDiv, progressSection.parentNode.children[2]);
        progressSection.style.marginTop = "10px";
        progressSection.style.marginBottom = "10px";
    };
};

function updateStreak(newStreak) {
    if (newStreak === ERROR_RESP) {
        if (document.getElementById("country-streak2") != null && (!!document.querySelector('div[data-qa="guess-description"]'))) {
            document.getElementById("country-streak2").innerHTML =
                `<div><i>Country codes could not be fetched. If your API key is new, it should activate soon.</i></div>
                 <div><i>Check for typos in the API key. You might also see this message if bigdatacloud is down</i></div>
                 <div><i>or in the unlikely event that you have exceeded you quota limit of 50,000 requests.</i></div>
                 <div><i>In the meantime, you can press 1 to count the country as correct, or press 0 otherwise.</i></div>`;
        }
        return;
    }
    sessionStorage.setItem("Streak", newStreak);
    if (!(streak > 0 && newStreak == 0)) {
        sessionStorage.setItem("StreakBackup", newStreak);
    };
    if (document.getElementById("country-streak") != null) {
        document.getElementById("country-streak").innerHTML = newStreak;
    };
    if (document.getElementById("country-streak2") != null) {
        document.getElementById("country-streak2").innerHTML = `<h2><i>Country Streak: ${newStreak}</i></h2>`;
        if (newStreak == 0) {
            if (streak >= 2) {
                document.getElementById("country-streak2").innerHTML = `<h2><i>Country Streak: 0</i></h2>
                    Your streak ended after correctly guessing ${geoguessrStyle(streak)} countries in a row.`;
            } else if (streak == 1) {
                document.getElementById("country-streak2").innerHTML = `<h2><i>Country Streak: 0</i></h2>
                    Your streak ended after correctly guessing ${geoguessrStyle(1)} country.`;
            };
        };
    };
    streak = newStreak;
};

async function getCountryCode(coords) {
    if (coords[0] <= -85.05) return 'AQ';
    const api = "https://api.bigdatacloud.net/data/reverse-geocode?latitude="+coords.lat+"&longitude="+coords.lng+"&localityLanguage=en&key="+API_Key
    return await fetch(api)
        .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
        .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out.countryCode]);
};

let lastGuess = { lat: 91, lng: 0 };
function check() {
    const gameTag = location.href.substring(location.href.lastIndexOf('/') + 1)
    let apiUrl = "https://www.geoguessr.com/api/v3/games/"+gameTag;
    if (location.pathname.startsWith("/challenge/")) {
        apiUrl = "https://www.geoguessr.com/api/v3/challenges/"+gameTag+"/game";
    };
    fetch(apiUrl)
    .then(res => res.json())
    .then((out) => {
        const guessCounter = out.player.guesses.length;
        const round = out.rounds[guessCounter-1];
        const guess = out.player.guesses[guessCounter-1];
        if (guess.lat == lastGuess.lat && guess.lng == lastGuess.lng) return;
        lastGuess = guess;
        Promise.all([getCountryCode(guess), getCountryCode(round)]).then(codes => {
            if (codes[0] == ERROR_RESP || codes[1] == ERROR_RESP) {
                updateStreak(ERROR_RESP);
            } else if (codes[0] == codes[1]) {
                updateStreak(streak + 1);
            } else {
                updateStreak(0);
            };
        });
    }).catch(err => { throw err });
};

function doCheck() {
    if (!document.querySelector('div[class*="result-layout_root__"]')) {
        sessionStorage.setItem("Checked", 0);
    } else if ((sessionStorage.getItem("Checked") || 0) == 0) {
        check();
        sessionStorage.setItem("Checked", 1);
    }
};

let lastDoCheckCall = 0;
new MutationObserver(async (mutations) => {
    if (!checkGameMode() || lastDoCheckCall >= (Date.now() - 50)) return;
    lastDoCheckCall = Date.now();
    await scanStyles()
    if (_AUTOMATIC) doCheck();
    addStreakStatusBar();
    addStreakRoundResult();
    addStreakGameSummary();
}).observe(document.body, { subtree: true, childList: true });

document.addEventListener('keypress', (e) => {
    if (e.key == '1') {
        updateStreak(streak + 1);
    } else if (e.key == '2') {
        updateStreak(streak - 1);
    } else if (e.key == '8') {
        const streakBackup = parseInt(sessionStorage.getItem("StreakBackup") || 0, 10);
        updateStreak(streakBackup + 1);
    } else if (e.key == '0') {
        updateStreak(0);
        sessionStorage.setItem("StreakBackup", 0);
    };
});