您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The script that makes pinpointing matter. Read more at the GeoClassics discord server or check out Pinpointing Tournaments on twitch.tv/GeoClassics.
// ==UserScript== // @name Pinpointing Duels // @namespace http://tampermonkey.net/ // @version 2.1.0 // @description The script that makes pinpointing matter. Read more at the GeoClassics discord server or check out Pinpointing Tournaments on twitch.tv/GeoClassics. // @match https://www.geoguessr.com/* // @icon https://i.imgur.com/eKp3nIa.png // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { let movementAllowed = false; GM_registerMenuCommand("Settings", showSettingsPanel); function showSettingsPanel() { if (document.getElementById('settings-panel')) return; const firstToValue = GM_getValue('firstTo', 10); const tieRangeValue = GM_getValue('tieRange', 0); const horizontalValue = GM_getValue('scoreBoxOffset', 30); const verticalValue = GM_getValue('scoreBoxTop', 86); const panel = document.createElement('div'); panel.id = 'settings-panel'; panel.style.cssText = ` position: fixed; top: 100px; right: 100px; background: #171717; color: white; padding: 20px; z-index: 10000; border-radius: 10px; font-family: sans-serif; width: 300px; `; // FIRST TO const firstToLabel = document.createElement('label'); firstToLabel.innerText = "First to:"; const firstToSelect = document.createElement('select'); for (let x = 3; x <= 30; x += 1) { const option = document.createElement('option'); option.value = x; option.innerText = x; if (x == firstToValue) option.selected = true; firstToSelect.appendChild(option); } // TIE RANGE const tieLabel = document.createElement('label'); tieLabel.innerText = "Tie Range Mode:"; const tieSelect = document.createElement('select'); [ { value: 0, label: "Disabled" }, { value: 1, label: "Full Tie Range" }, { value: 2, label: "Half Tie Range" } ].forEach(({ value, label }) => { const option = document.createElement('option'); option.value = value; option.innerText = label; if (value == tieRangeValue) option.selected = true; tieSelect.appendChild(option); }); // HORIZONTAL POSITION const hLabel = document.createElement('label'); hLabel.innerText = `Score Board Horizontal Position: ${horizontalValue}%`; hLabel.style.display = "block"; const hSlider = document.createElement('input'); hSlider.type = "range"; hSlider.min = "0"; hSlider.max = "100"; hSlider.value = horizontalValue; hSlider.style.width = "100%"; hSlider.addEventListener('input', () => { hLabel.innerText = `Score Board Horizontal Position: ${hSlider.value}%`; updateScoreBoxPosition(parseInt(hSlider.value), parseInt(vSlider.value)); }); // VERTICAL POSITION const vLabel = document.createElement('label'); vLabel.innerText = `Score Board Vertical Position: ${verticalValue}px`; vLabel.style.display = "block"; const vSlider = document.createElement('input'); vSlider.type = "range"; vSlider.min = "0"; vSlider.max = "300"; vSlider.value = verticalValue; vSlider.style.width = "100%"; vSlider.addEventListener('input', () => { vLabel.innerText = `Score Board Vertical Position: ${vSlider.value}px`; updateScoreBoxPosition(parseInt(hSlider.value), parseInt(vSlider.value)); }); // Buttons const saveBtn = document.createElement('button'); saveBtn.innerText = "Save"; saveBtn.style.margin = "10px"; saveBtn.style.cursor = "pointer"; saveBtn.style.color = "white"; saveBtn.onclick = () => { firstTo = parseInt(firstToSelect.value); tieRange = parseInt(tieSelect.value); winThreshold = firstTo; GM_setValue('firstTo', firstTo); GM_setValue('tieRange', tieRange); GM_setValue('scoreBoxOffset', parseInt(hSlider.value)); GM_setValue('scoreBoxTop', parseInt(vSlider.value)); alert("Settings saved!"); panel.remove(); }; const cancelBtn = document.createElement('button'); cancelBtn.innerText = "Cancel"; cancelBtn.style.cursor = "pointer"; cancelBtn.style.color = "white"; cancelBtn.onclick = () => panel.remove(); // Add to DOM panel.append( firstToLabel, firstToSelect, document.createElement("br"), document.createElement("br"), tieLabel, tieSelect, document.createElement("br"), document.createElement("br"), hLabel, hSlider, document.createElement("br"), document.createElement("br"), vLabel, vSlider, document.createElement("br"), document.createElement("br"), saveBtn, cancelBtn ); document.body.appendChild(panel); } // Resolve a *single* CSS-module class by prefix function cn(prefix) { const el = document.querySelector(`[class^="${prefix}"]`); if (!el) return prefix; // fallback so code doesn't crash const cls = [...el.classList].find(c => c.startsWith(prefix)); return cls || prefix; } function onDuelsUIReady(cb) { if (q("duels_hud__")) return cb(); const obs = new MutationObserver(() => { if (q("duels_hud__")) { obs.disconnect(); cb(); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); } // Shorthands for querying with module prefixes const q = (pref, root = document) => root.querySelector(`[class^="${pref}"]`); const qa = (pref, root = document) => root.querySelectorAll(`[class^="${pref}"]`); // Match either of multiple prefixes const qAny = (prefs, root = document) => root.querySelector(prefs.map(p => `[class^="${p}"]`).join(',')); const qaAny = (prefs, root = document) => root.querySelectorAll(prefs.map(p => `[class^="${p}"]`).join(',')); function updateScoreBoxPosition(horizontalPercent, verticalPx) { const leftEl = document.getElementById("leftScore"); const rightEl = document.getElementById("rightScore"); if (leftEl) { leftEl.style.left = `${horizontalPercent}%`; leftEl.style.top = `${verticalPx}px`; } if (rightEl) { rightEl.style.right = `${horizontalPercent}%`; rightEl.style.top = `${verticalPx}px`; } } function getRoundHeaderContainer() { return qAny([ "damage-animation_root__", "round-score-header_roundNumber__" ]); } function getHudMountRoot() { return document.body; // had to fallback to body cause the disappearing divs are not suitable } function mountWithAnchor(wrapper, anchor) { const mount = getHudMountRoot(); mount.appendChild(wrapper); const mo = new MutationObserver(() => { if (!anchor.isConnected) { wrapper.remove(); mo.disconnect(); } }); mo.observe(document.documentElement, { childList: true, subtree: true }); } function showResultDisplay(message) { const anchor = q("damage-animation_root__"); if (!anchor) return; if (document.getElementById("roundDisplayMessage")) return; const wrapper = document.createElement("div"); wrapper.id = "roundDisplayMessage"; wrapper.innerHTML = ` <div style=" position: fixed; left: 50%; transform: translateX(-50%); top: 35%; padding: 16px 20px; font-size: 24px; line-height: 24px; text-align: center; color: #f2f2f2; background: linear-gradient(180deg,#322a6a 0%, #594eaf 100%); border: 7px solid #8f86e6; border-radius: 8px; box-shadow: 0 2px 0 rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.08); z-index: 9998; ">${message}</div> `; mountWithAnchor(wrapper, anchor); } function showPenaltyDisplay(message) { const anchor = q("damage-animation_root__"); if (!anchor) return; if (document.getElementById("roundDisplayPenalty")) return; const wrapper = document.createElement("div"); wrapper.id = "roundDisplayPenalty"; wrapper.innerHTML = ` <div style=" position: fixed; left: 50%; transform: translateX(-50%); top: 55%; padding: 16px 20px; font-size: 24px; line-height: 24px; text-align: center; color: #f2f2f2; background: linear-gradient(180deg, #322a6a 0%, #594eaf 100%); border: 7px solid #8f86e6; border-radius: 8px; box-shadow: 0 2px 0 rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.08); z-index: 9998; ">SCORE 0 FOR GUESSING EARLY AND MISSING 5K: ${message}</div> `; mountWithAnchor(wrapper, anchor); } 'use strict'; let firstTo = GM_getValue('firstTo', 10); // Fallback 10 let winThreshold = firstTo; //Win Value let tieRange = GM_getValue('tieRange', 0); let leftScore = 0, rightScore = 0; let gameOver = false; // Flag to ensure the end screen is only shown once let currentDuel = false; // Flag to ensure the end screen is only shown once // Extract the logged-in player's ID from __NEXT_DATA__ const getLoggedInUserId = () => { const element = document.getElementById("__NEXT_DATA__"); if (!element) return null; let exto = JSON.parse(element.innerText).props.accountProps.account.user.userId return exto; }; // Determine which team (0 or 1) the logged-in user belongs to. const getLoggedInUserTeamIndex = (teams, loggedInUserId) => { for (let i = 0; i < teams.length; i++) { if (teams[i].players && teams[i].players.some(player => player.playerId === loggedInUserId)) { return i; } } return null; }; function modifyHealthBars() { if (!movementAllowed) return; const healthContainer = qAny(["hud-2_healthBars__", "hud_healthBars__"]); if (!healthContainer) return; qaAny([ "health-bar-2_barContainer__", "health-bar-2_barSkewContainer__", "health-bar-2_barBackground__", "health-bar-2_bar__", "health-bar-2_barLabel__" ]).forEach(el => el && el.style.setProperty("display", "none", "important")); qaAny(["health-bar-2_barInnerContainer__", "health-bar_barInnerContainer__"]) .forEach(c => c && (c.style.background = "none")); qaAny([ "health-bar-2_playerContainer__", // legacy "health-bar_playerContainer__", // legacy "health-bar-2_nickContainer__" // current ]).forEach(c => { if (!c) return; c.style.top = "0.5rem"; c.style.marginTop = "0.5rem"; }); if (!document.getElementById("leftScore") || !document.getElementById("rightScore")) { createScoreDisplays(); } } // Create score display divs. const createScoreDisplays = () => { const hudRoot = q("duels_hud__"); if (!hudRoot) return; const createScoreDiv = (id, position) => { const div = document.createElement("div"); div.id = id; div.innerText = (id === "leftScore") ? leftScore : rightScore; div.style.cssText = ` padding: 10px 20px; font-size: 36px; font-weight: bold; color: white; background: linear-gradient(180deg,#322a6a 0%, #594eaf 100%); border: 7px solid #8f86e6; border-radius: 8px; text-align: center; margin: 5px; position: absolute; top: 20px; ${position}: 320px; z-index: 1000; `; return div; }; hudRoot.appendChild(createScoreDiv("leftScore", "left")); hudRoot.appendChild(createScoreDiv("rightScore", "right")); }; const showEndScreen = () => { const overlay = document.createElement("div"); overlay.id = "endScreenOverlay"; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: linear-gradient(180deg,rgba(44,44,44,1),rgba(55,55,55,1) 95%),#555; color: #f0f0f0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 9999; text-align: center; `; const winnerText = (leftScore >= winThreshold) ? "YOU WIN THE GAME!" : "OPPONENT WINS THE GAME!"; const scoreText = `${leftScore}-${rightScore}`; const winnerElem = document.createElement("div"); winnerElem.innerText = winnerText; winnerElem.style.cssText = ` font-size: 72pt; margin-bottom: 20px; `; const scoreElem = document.createElement("div"); scoreElem.innerText = scoreText; scoreElem.style.cssText = ` font-size: 90pt; margin-bottom: 20px; color: white; `; const messageElem = document.createElement("div"); messageElem.innerText = "Guess Antarctica for the remaining rounds if you need to get a summary link."; messageElem.style.cssText = ` font-size: 24pt; margin-bottom: 20px; color: white; `; const button = document.createElement("button"); button.innerText = "Okay"; button.setAttribute("style", ` background-image: linear-gradient(rgb(151, 232, 81), rgb(71, 148, 64)); border-radius: 60px; box-shadow: rgba(0, 0, 0, 0.25) 0px 4.4px 18px 0px, rgba(255, 255, 255, 0.2) 0px 1px 0px 0px inset, rgba(0, 0, 0, 0.3) 0px -2px 0px 0px inset; color: rgb(255, 255, 255); cursor: pointer; display: inline-block; font-family: ggFont, sans-serif; font-style: italic; font-weight: 700; font-size: 14px; height: 44px; margin-top: 4px; padding: 0 12px; position: relative; text-align: center; text-shadow: rgb(23, 18, 53) 0px 1px 2px; text-transform: uppercase; transition: transform 0.15s ease, background 0.15s ease; user-select: none; width: 65px; -webkit-font-smoothing: antialiased; `); button.addEventListener("click", () => { overlay.remove(); }); overlay.appendChild(winnerElem); overlay.appendChild(scoreElem); overlay.appendChild(messageElem); overlay.appendChild(button); document.body.appendChild(overlay); }; // Flash the score element by toggling backgrounds every 0.5s for 5s. const flashScore = (scoreElement) => { const originalBackground = scoreElement.style.background; const flashBackground = "linear-gradient(180deg,rgba(90,219,149,1),rgba(60,200,110,1) 95%),#5adb95"; let flashCount = 0; const interval = setInterval(() => { scoreElement.style.background = (flashCount % 2 === 0) ? flashBackground : originalBackground; flashCount++; if (flashCount >= 10) { clearInterval(interval); scoreElement.style.background = originalBackground; } }, 500); }; const updateScores = (response) => { let team0Score = 0, team1Score = 0; let newTeam0Score = 0, newTeam1Score = 0; let tieDistance = 0; const timeAfterGuess = response.options.roundTime; const maxRoundTime = response.options.maxRoundTime; if (response.teams && response.teams.length >= 2) { const loggedInUserId = getLoggedInUserId(); const teamIndex = getLoggedInUserTeamIndex(response.teams, loggedInUserId); for (let i = 0; i < response.currentRoundNumber && newTeam0Score < winThreshold && newTeam1Score < winThreshold; i++) { if (!response.rounds[i].hasProcessedRoundTimeout) continue; const oldDisplayMessage = document.getElementById("roundDisplayMessage"); if (oldDisplayMessage) oldDisplayMessage.remove(); const oldPenaltyMessage = document.getElementById("roundDisplayPenalty"); if (oldPenaltyMessage) oldPenaltyMessage.remove(); let message = '', tieMessage = '', winMessage = ''; let roundStartTime = new Date(response.rounds[i]?.startTime)/1000 || Infinity; let roundEndTime = new Date(response.rounds[i]?.endTime)/1000 || Infinity; let team0RoundScore = 0,team1RoundScore = 0; let team0Time = Infinity, team1Time = Infinity; let team0BestScore = 0, team1BestScore = 0; for (let j = 0; j < response.teams[0]?.players.length; j++){ let currentGuess; response.teams[0].players[j].guesses.forEach((guess) => {if (guess.roundNumber == i+1) currentGuess = guess;}); if (currentGuess === undefined) continue; let guessTime = new Date(currentGuess.created)/1000 || Infinity; let score = Math.round(5000*Math.exp(-10*currentGuess.distance/response.options?.map?.maxErrorDistance)) || 0; score = currentGuess.distance < 25 ? 5000 : score; if (guessTime < team0Time && guessTime < roundEndTime) { team0Time = guessTime; team0RoundScore = score; } else if (score > team0BestScore) { team0BestScore = score; } } if (team0Time > roundEndTime) team0RoundScore = team0BestScore; for (let j = 0; j < response.teams[1]?.players.length; j++){ let currentGuess; response.teams[1].players[j].guesses.forEach((guess) => {if (guess.roundNumber == i+1) currentGuess = guess;}); if (currentGuess === undefined) continue; let guessTime = new Date(currentGuess.created)/1000 || Infinity; let score = Math.round(5000*Math.exp(-10*currentGuess.distance/response.options?.map?.maxErrorDistance)) || 0; score = currentGuess.distance < 25 ? 5000 : score; if (guessTime < team1Time && guessTime < roundEndTime) { team1Time = guessTime; team1RoundScore = score; } else if (score > team1BestScore) { team1BestScore = score; } } if (team1Time > roundEndTime) team1RoundScore = team1BestScore; if (team0RoundScore < 5000 && team0Time < team1Time && team0Time < (roundStartTime + maxRoundTime - timeAfterGuess)) { showPenaltyDisplay(teamIndex === 0 ? "YOU" : "YOUR OPPONENT"); team0RoundScore = 0; } else if (team1RoundScore < 5000 && team1Time < team0Time && team1Time < (roundStartTime + maxRoundTime - timeAfterGuess)) { showPenaltyDisplay(teamIndex === 1 ? "YOU" : "YOUR OPPONENT"); team1RoundScore = 0; } // Score counted as 0 if penalty for early sending team0Score = newTeam0Score; team1Score = newTeam1Score; const highestScore = team0RoundScore > team1RoundScore ? team0RoundScore : team1RoundScore; if (tieRange > 0) tieDistance = Math.floor((5000 - highestScore) / tieRange); if (team0RoundScore === 5000 && team1RoundScore === 5000) { if (team0Time < team1Time) newTeam0Score++; else if (team1Time < team0Time) newTeam1Score++; message = `1 POINT FOR FASTEST 5k`; } else if (team0RoundScore == 5000 || team1RoundScore == 5000) { if (team0RoundScore == 5000) newTeam0Score += 2; else newTeam1Score += 2; message = `2 POINTS FOR SOLO 5K`; } else if (team0RoundScore > team1RoundScore + tieDistance || team1RoundScore > team0RoundScore + tieDistance) { if (team0RoundScore > team1RoundScore + tieDistance) newTeam0Score++; else newTeam1Score ++; message = `1 POINT FOR CLOSEST GUESS`; if (tieRange>0) tieMessage = `<br><span style="font-size: 14px;">TIE RANGE: ${tieDistance} POINTS</span>`; } else { message = `TIE!`; if (tieRange>0) tieMessage = `<br><span style="font-size: 14px;">TIE RANGE: ${tieDistance} POINTS</span>`; } if (message !== `TIE!`) winMessage = (teamIndex === 0) === (newTeam0Score > team0Score) ? "YOU WIN THE ROUND! <br>" : "YOUR OPPONENT WINS THE ROUND! <br>"; const isMatchPoint = newTeam0Score >= winThreshold - 2 || newTeam1Score >= winThreshold - 2; showResultDisplay(`${winMessage} ${message} ${isMatchPoint ? "<br>MATCH POINT!" : "" } ${tieMessage}`); } const leftScoreChanged = (teamIndex === 0 ? newTeam0Score !== leftScore : newTeam1Score !== leftScore); const rightScoreChanged = (teamIndex === 0 ? newTeam1Score !== rightScore : newTeam0Score !== rightScore); if (teamIndex === 0) { leftScore = newTeam0Score; rightScore = newTeam1Score; } else if (teamIndex === 1) { leftScore = newTeam1Score; rightScore = newTeam0Score; } else { leftScore = newTeam0Score; rightScore = newTeam1Score; } const isMatchPoint = leftScore >= winThreshold - 2 || rightScore >= winThreshold - 2; const leftScoreEl = document.getElementById("leftScore"); const rightScoreEl = document.getElementById("rightScore"); if (leftScoreEl) { leftScoreEl.innerText = leftScore; if (leftScoreChanged) flashScore(leftScoreEl); } if (rightScoreEl) { rightScoreEl.innerText = rightScore; if (rightScoreChanged) flashScore(rightScoreEl); } if (!gameOver && (leftScore >= winThreshold || rightScore >= winThreshold)) { gameOver = true; showEndScreen(); } } }; async function fetchDuelData() { const duelId = location.pathname.split("/")[2]; if (!duelId) return; if (gameOver) { if (duelId !== currentDuel) { gameOver = false; } else { return; } } currentDuel = duelId; const res = await fetch( `https://game-server.geoguessr.com/api/duels/${duelId}`, { method: "GET", credentials: "include" } ); const data = await res.json(); const { forbidMoving, forbidZooming, forbidRotating } = data.options.movementOptions; movementAllowed = !(forbidMoving || forbidZooming || forbidRotating); if (!movementAllowed) { return; } modifyHealthBars(); updateScores(data); } if (location.href.includes("/duels/")) { onDuelsUIReady(fetchDuelData); } // Listen for URL changes to auto-activate the script. (function() { const _wr = type => { const orig = history[type]; return function() { const rv = orig.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); return rv; }; }; history.pushState = _wr("pushState"); history.replaceState = _wr("replaceState"); window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); })(); window.addEventListener('locationchange', function(){ if (location.href.includes("/duels/")) { onDuelsUIReady(fetchDuelData); } }); setInterval(() => { if (location.href.includes("/duels/")) { onDuelsUIReady(fetchDuelData); } }, 2500); if (location.href.includes("/team-duels/")) { onDuelsUIReady(fetchDuelData); } // Listen for URL changes to auto-activate the script. (function() { const _wr = type => { const orig = history[type]; return function() { const rv = orig.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); return rv; }; }; history.pushState = _wr("pushState"); history.replaceState = _wr("replaceState"); window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); })(); window.addEventListener('locationchange', function(){ if (location.href.includes("/team-duels/")) { onDuelsUIReady(fetchDuelData); } }); setInterval(() => { if (location.href.includes("/team-duels/")) { onDuelsUIReady(fetchDuelData); } }, 2500); })();