您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Replace 1500+ levels with special ranks
当前为
// ==UserScript== // @name GeoGuessr High Level Ranks // @version 1.5.1 // @description Replace 1500+ levels with special ranks // @match https://www.geoguessr.com/* // @icon https://i.imgur.com/wHQjX4m.png // @license MIT // @run-at document-idle // @grant GM_addStyle // @namespace https://example.com/ // ==/UserScript== (function () { 'use strict'; // ----CHANGE THIS----- const RECOLOR_TOGGLE = "on"; // "on" = Enable Rank Background, "off" = Disable Rank Background // -------------------- const STYLE_ID = 'gg-ranks-style'; // style element id for recolor css (so we can remove/replace it) // Badge sets const BADGES_DIVISION = [ { min: 1500, max: 1649, url: 'https://i.imgur.com/aR6fova.png' }, { min: 1650, max: 1799, url: 'https://i.imgur.com/No26QT6.png' }, { min: 1800, max: 1999, url: 'https://i.imgur.com/DH3XBSr.png' }, { min: 2000, max: 2199, url: 'https://i.imgur.com/mTCZKHg.png' }, { min: 2200, max: Infinity, url: 'https://i.imgur.com/wHQjX4m.png' }, ]; const BADGES_MULTIPLAYER = [...BADGES_DIVISION]; // Team duels: rating < 1350 => no change const BADGES_TEAMDUEL = [ { min: 1350, max: 1399, url: 'https://i.imgur.com/GYUETku.png' }, { min: 1400, max: 1499, url: 'https://i.imgur.com/QPo1lET.png' }, { min: 1500, max: 1599, url: 'https://i.imgur.com/QLW7KyP.png' }, { min: 1600, max: 1699, url: 'https://i.imgur.com/1K4mAXB.png' }, { min: 1700, max: Infinity, url: 'https://i.imgur.com/rZsaPIw.png' }, ]; // Titles const TITLES = [ { min: 1500, max: 1649, label: 'Grand Champion 3' }, { min: 1650, max: 1799, label: 'Grand Champion 2' }, { min: 1800, max: 1999, label: 'Grand Champion 1' }, { min: 2000, max: 2199, label: 'Legend' }, { min: 2200, max: Infinity, label: 'Eternal' }, ]; const TITLES_TEAMDUEL = [ { min: 1350, max: 1399, label: 'Grand Champion 3' }, { min: 1400, max: 1499, label: 'Grand Champion 2' }, { min: 1500, max: 1599, label: 'Grand Champion 1' }, { min: 1600, max: 1699, label: 'Legend' }, { min: 1700, max: Infinity, label: 'Eternal' }, ]; // -------------------- // Utilities // -------------------- function extractFirstInteger(text) { if (!text) return null; const cleaned = String(text).replace(/,/g, '').trim(); const m = cleaned.match(/(\d{2,5})/); if (!m) return null; const n = parseInt(m[1], 10); return Number.isFinite(n) ? n : null; } function pickForRating(arr, rating) { if (rating == null) return null; for (const e of arr) { if (rating >= e.min && rating <= e.max) return e.url || e.label || null; } return null; } function pickTitleForRatingFromArray(arr, rating) { if (rating == null) return null; for (const t of arr) { if (rating >= t.min && rating <= t.max) return t.label; } return null; } function pickTitleForRating(rating) { return pickTitleForRatingFromArray(TITLES, rating); } // -------------------- // Style helper (create/remove style element so we can cleanly reset) // -------------------- let styleEl = null; function ensureStyleEl() { if (!styleEl) { styleEl = document.getElementById(STYLE_ID); if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = STYLE_ID; document.head && document.head.appendChild(styleEl); } } return styleEl; } function setHeaderCss(cssText) { const s = ensureStyleEl(); s.textContent = cssText || ''; } function clearHeaderCss() { if (styleEl || document.getElementById(STYLE_ID)) { const s = styleEl || document.getElementById(STYLE_ID); s.textContent = ''; } } // -------------------- // Header recolor (prefix selectors to match variable class suffixes) // -------------------- function recolorHeader(rating, isTeamDuel = false) { if (RECOLOR_TOGGLE.toLowerCase() !== "on") { clearHeaderCss(); return; } let background = null; let overlay = null; let overlayOpacity = 1.0; if (isTeamDuel) { if (rating >= 1350 && rating <= 1599) { background = "linear-gradient(179deg, #8b0000 -3.95%, #ff0000 95.2%)"; overlay = "linear-gradient(41deg, #330613, #bf1755)"; overlayOpacity = 0.7; } else if (rating >= 1600 && rating <= 1699) { background = "linear-gradient(179deg, #b8860b -3.95%, #ffd700 95.2%)"; overlay = "linear-gradient(41deg, #2b1900, #d68940)"; overlayOpacity = 0.75; } else if (rating >= 1700) { background = "linear-gradient(179deg, #ffdee3 -3.95%, #ffdbe2 95.2%)"; overlay = "linear-gradient(41deg, #5e4d5b, #c2089a)"; overlayOpacity = 0.6; } else { // nothing matched -> clear clearHeaderCss(); return; } } else { if (rating >= 1500 && rating <= 1999) { background = "linear-gradient(179deg, #8b0000 -3.95%, #ff0000 95.2%)"; overlay = "linear-gradient(41deg, #330613, #bf1755)"; overlayOpacity = 0.7; } else if (rating >= 2000 && rating <= 2199) { background = "linear-gradient(179deg, #b8860b -3.95%, #ffd700 95.2%)"; overlay = "linear-gradient(41deg, #2b1900, #d68940)"; overlayOpacity = 0.75; } else if (rating >= 2200) { background = "linear-gradient(179deg, #ffdee3 -3.95%, #ffdbe2 95.2%)"; overlay = "linear-gradient(41deg, #5e4d5b, #c2089a)"; overlayOpacity = 0.6; } else { clearHeaderCss(); return; } } const css = ` [class^="division-header_background__"] { background: ${background} !important; } [class^="division-header_pattern__"]::before { opacity: 0 !important; } [class^="division-header_overlay__"] { background: ${overlay} !important; opacity: ${overlayOpacity} !important; } `; setHeaderCss(css); } // -------------------- // Robust helpers for multiplayer detection // -------------------- function labelWithDigits(root) { if (!root) return null; // prefer the specific shared_yellowVariant label if present const specific = root.querySelector('label.shared_yellowVariant__XONv8'); if (specific) return specific; // otherwise find the first label that contains a 2-5 digit number const labels = Array.from(root.querySelectorAll('label')); return labels.find(l => /\d{2,5}/.test((l.textContent || '').trim())) || null; } // -------------------- // Read current division/team rating and detect team-duel reliably // returns { rating: number|null, isTeamDuel: boolean } // -------------------- function getDivisionInfo() { const ratingEl = document.querySelector('[class^="division-header_rating__"]'); const titleEl = document.querySelector('[class^="division-header_title__"]'); if (!ratingEl) return { rating: null, isTeamDuel: false }; const rating = extractFirstInteger(ratingEl.textContent || ratingEl.innerText || ''); const ratingClass = String(ratingEl.className || ''); // Strict matching: only treat as team-duel when we specifically see // the 'division-header_rating__SHoXJ' token. This prevents false // positives from similarly-prefixed tokens like 'division-header_rating__CQOgo'. const isTeamDuel = /\bdivision-header_rating__SHoXJ\b/.test(ratingClass); return { rating, isTeamDuel }; } // -------------------- // Reset logic (minimal) — restoration removed by user request // -------------------- function resetAll() { // clear recolor CSS only clearHeaderCss(); // clear any scheduled updates so we start fresh if (scheduled) { clearTimeout(scheduled); scheduled = null; } } // -------------------- // Division area update (supports variable class suffixes, team-duel title class like g2AVE) // Restoration code removed: if no badge/title found we now leave DOM as-is. // -------------------- function updateDivisionArea() { const info = getDivisionInfo(); const rating = info.rating; const isTeamDuel = info.isTeamDuel; const badgeEl = document.querySelector('[class^="division-header_badge__"], img[class^="division-header_badge__"]'); const titleEl = document.querySelector('[class^="division-header_title__"]'); // catches g2AVE, 3YYUS, etc. if (rating == null && !badgeEl && !titleEl) return false; const badgeArray = isTeamDuel ? BADGES_TEAMDUEL : BADGES_DIVISION; const titleArray = isTeamDuel ? TITLES_TEAMDUEL : TITLES; const badgeUrl = pickForRating(badgeArray, rating); const titleStr = pickTitleForRatingFromArray(titleArray, rating); if (!isNaN(rating)) recolorHeader(rating, isTeamDuel); // Badge handling (works even if the page already has data-orig-src / data-orig-srcset) if (badgeEl && badgeEl.tagName === 'IMG' && badgeUrl) { badgeEl.dataset.origSrc = badgeEl.dataset.origSrc || badgeEl.getAttribute('data-orig-src') || badgeEl.getAttribute('src') || ''; badgeEl.dataset.origSrcset = badgeEl.dataset.origSrcset || badgeEl.getAttribute('data-orig-srcset') || badgeEl.getAttribute('srcset') || ''; const cur = badgeEl.getAttribute('src') || ''; if (!cur.includes(badgeUrl)) { badgeEl.setAttribute('src', badgeUrl); badgeEl.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`); badgeEl.dataset.replaced = 'true'; console.log('[GG Division] Replaced division badge ->', badgeUrl, 'rating:', rating, 'teamDuel:', isTeamDuel); } } // Title handling (only change if we have a title for this rating) if (titleEl && titleStr) { if (!('origTitle' in titleEl.dataset)) { titleEl.dataset.origTitle = (titleEl.textContent || '').trim(); titleEl.dataset.origDataOriginalTitle = titleEl.getAttribute('data-original-title') || ''; } const cur = (titleEl.textContent || '').trim(); if (cur !== titleStr) { titleEl.textContent = titleStr; titleEl.dataset.replacedTitle = 'true'; console.log('[GG Division] Set division title ->', titleStr, 'rating:', rating, 'teamDuel:', isTeamDuel); } } return true; } // -------------------- // Team badges update — updates team-matchmaking header badge and rating_wrapper images when on a team duel // Restoration removed: if no badgeUrl we simply do nothing. // -------------------- function updateTeamBadges(info) { if (!info) info = getDivisionInfo(); const rating = info.rating; const isTeamDuel = info.isTeamDuel; if (!isTeamDuel || rating == null) return false; const badgeUrl = pickForRating(BADGES_TEAMDUEL, rating); if (!badgeUrl) return false; // team matchmaking header badge(s) (prefix selector to handle dynamic suffixes) const teamHeaderImgs = Array.from(document.querySelectorAll('img[class^="team-matchmaking-layout_badge__"], [class^="team-matchmaking-layout_badge__"]')); teamHeaderImgs.forEach(img => { if (img && img.tagName === 'IMG') { img.dataset.origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src') || img.getAttribute('src') || ''; img.dataset.origSrcset = img.dataset.origSrcset || img.getAttribute('data-orig-srcset') || img.getAttribute('srcset') || ''; const cur = img.getAttribute('src') || ''; if (!cur.includes(badgeUrl)) { img.setAttribute('src', badgeUrl); img.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`); img.dataset.replaced = 'true'; console.log('[GG Team] Replaced team-matchmaking badge ->', badgeUrl, 'rating:', rating); } } }); // rating_wrapper images (example: .rating_wrapper__22uFu img) const ratingWrapperImgs = Array.from(document.querySelectorAll('.rating_wrapper__22uFu img, [class^="rating_wrapper__22uFu"] img')); ratingWrapperImgs.forEach(img => { if (img && img.tagName === 'IMG') { img.dataset.origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src') || img.getAttribute('src') || ''; img.dataset.origSrcset = img.dataset.origSrcset || img.getAttribute('data-orig-srcset') || img.getAttribute('srcset') || ''; const cur = img.getAttribute('src') || ''; if (!cur.includes(badgeUrl)) { img.setAttribute('src', badgeUrl); img.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`); img.dataset.replaced = 'true'; console.log('[GG Team] Replaced rating_wrapper badge ->', badgeUrl, 'rating:', rating); } } }); return true; } // -------------------- // Multiplayer boxes (robust) // -------------------- function findMultiplayerBoxes() { // primary selector (unchanged) let boxes = Array.from(document.querySelectorAll('.multiplayer_ratingBox__05Gko')); if (boxes.length) return boxes; // fallback: search inside multiplayer root for elements that contain a label with digits const root = document.querySelector('.multiplayer_root__jmpXA'); if (!root) return []; const candidates = Array.from(root.querySelectorAll('div,section,article')); const filtered = candidates.filter(el => { // accept if there's any label with a 2-5 digit number const lbl = Array.from(el.querySelectorAll('label')).some(l => /\d{2,5}/.test((l.textContent || '').trim())); return lbl; }); // remove nested duplicates (keep top-level ones) const topLevel = filtered.filter((el, i, arr) => !arr.some(other => other !== el && other.contains(el))); return topLevel; } // -------------------- // New helpers: find all digit labels and pair boxes to labels by proximity // -------------------- function findAllDigitLabels() { return Array.from(document.querySelectorAll('label')) .filter(l => /\d{2,5}/.test((l.textContent || '').trim())); } // Pair boxes to labels by proximity (prefer vertical proximity) function pairBoxesToLabels(boxes) { const labels = findAllDigitLabels(); if (!boxes.length || !labels.length) return new Map(); const assigned = new Set(); const mapping = new Map(); for (const box of boxes) { const br = box.getBoundingClientRect(); let best = null; let bestScore = Infinity; for (const lbl of labels) { if (assigned.has(lbl)) continue; const lr = lbl.getBoundingClientRect(); // distance: weight vertical more than horizontal // (use fallback if getBoundingClientRect returns zeros) let dy = Math.abs((lr.top || 0) - (br.top || 0)); let dx = Math.abs((lr.left || 0) - (br.left || 0)); let score = dy * 2 + dx; // If rects are zero (not rendered), use DOM order distance as fallback if ((!lr.width && !lr.height) || (!br.width && !br.height)) { const li = Array.prototype.indexOf.call(labels, lbl); const bi = Array.prototype.indexOf.call(boxes, box); score = Math.abs(li - bi) * 1000; // coarse fallback } if (score < bestScore) { bestScore = score; best = lbl; } } if (best) { mapping.set(box, best); assigned.add(best); } } return mapping; } // -------------------- // Updated multiplayer box update: accepts optional ratingLabelOverride and isTeamDuel flag // Restoration removed: if no badge/title found we leave the DOM unchanged. // -------------------- function updateMultiplayerBox(box, idx, ratingLabelOverride, isTeamDuelFlag) { // rating label robustly (use the override if provided) const ratingLabel = ratingLabelOverride || labelWithDigits(box); // title label: prefer the visible large label (your snippet shows label_label__9xkbh), but also accept generic label let titleLabel = box.querySelector('label[data-original-title]') || box.querySelector('label.label_label__9xkbh') || Array.from(box.querySelectorAll('label')).find(l => /[A-Za-z]/.test((l.textContent || '').trim())); const imgEl = box.querySelector('img.multiplayer_icon__hRbEa') || box.querySelector('img'); const rating = ratingLabel ? extractFirstInteger(ratingLabel.textContent || '') : null; // Choose arrays depending on whether this box should use team-duel assets const badgeArray = isTeamDuelFlag ? BADGES_TEAMDUEL : BADGES_MULTIPLAYER; const titleArray = isTeamDuelFlag ? TITLES_TEAMDUEL : TITLES; const badgeUrl = pickForRating(badgeArray, rating); const titleStr = pickTitleForRatingFromArray(titleArray, rating); // Image handling — only change if a badgeUrl exists if (imgEl && imgEl.tagName === 'IMG' && badgeUrl) { imgEl.dataset.origSrc = imgEl.dataset.origSrc || imgEl.getAttribute('data-orig-src') || imgEl.getAttribute('src') || ''; imgEl.dataset.origSrcset = imgEl.dataset.origSrcset || imgEl.getAttribute('data-orig-srcset') || imgEl.getAttribute('srcset') || ''; const cur = (imgEl.getAttribute('src') || ''); if (!cur.includes(badgeUrl)) { imgEl.setAttribute('src', badgeUrl); imgEl.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`); imgEl.dataset.replaced = 'true'; console.log('[GG MP] Replaced image for box', idx, '->', badgeUrl, 'rating:', rating, 'teamDuel:', !!isTeamDuelFlag); } } // Title handling — only change if we have a title for this rating if (titleLabel && titleStr) { if (!('origTitle' in titleLabel.dataset)) { titleLabel.dataset.origTitle = (titleLabel.textContent || '').trim(); titleLabel.dataset.origDataOriginalTitle = titleLabel.getAttribute('data-original-title') || ''; } const cur = (titleLabel.textContent || '').trim(); if (cur !== titleStr) { titleLabel.textContent = titleStr; try { titleLabel.setAttribute('data-original-title', titleStr); } catch (e) { /* ignore */ } titleLabel.dataset.replacedTitle = 'true'; console.log('[GG MP] Set title for box', idx, '->', titleStr, 'rating:', rating, 'teamDuel:', !!isTeamDuelFlag); } } } // -------------------- // Updated updateMultiplayerAll uses pairing and passes matched label into updateMultiplayerBox. // The second multiplayer box (index 1) is treated as a team-duel box and uses team-duel assets. // -------------------- function updateMultiplayerAll() { const boxes = findMultiplayerBoxes(); if (!boxes || !boxes.length) return false; // Pair boxes -> labels (nearest) const mapping = pairBoxesToLabels(boxes); boxes.forEach((b, i) => { try { const ratingLabel = mapping.get(b) || null; // Treat the second box as team-duel (index === 1) const isTeamDuelForThisBox = (i === 1); updateMultiplayerBox(b, i, ratingLabel, isTeamDuelForThisBox); } catch (e) { console.error('updateMultiplayerBox error', e); } }); return true; } // -------------------- // New: update team list entries (leaderboard/team pages) // - Finds column content elements (class prefix 'teams-detailed-leaderboard_columnContent__...') // - Reads the numeric rating label inside // - Locates the nearest team icon image in the same row and replaces it with BADGES_TEAMDUEL for that rating (if >=1350) // - Does not restore original images (per your request) // -------------------- function findNearestTeamIconFrom(el) { if (!el) return null; // Search up a few ancestor levels for a container that contains a team image let ancestor = el; for (let depth = 0; depth < 5 && ancestor; depth++) { // look for known patterns inside ancestor const img = ancestor.querySelector('img[class^="team-selector_divisionImage__"], [class^="team-selector_divisionImageWrapper__"] img, img.team-selector_divisionImage__U12_e, img'); if (img) return img; ancestor = ancestor.parentElement; } // fallback: scan siblings of el up to 3 siblings in either direction for an img let sib = el.previousElementSibling; for (let i = 0; i < 6 && sib; i++, sib = sib.previousElementSibling) { const img = sib.querySelector && sib.querySelector('img'); if (img) return img; } sib = el.nextElementSibling; for (let i = 0; i < 6 && sib; i++, sib = sib.nextElementSibling) { const img = sib.querySelector && sib.querySelector('img'); if (img) return img; } return null; } function updateTeamListEntries() { // column elements that contain rating values — prefix selector to be robust to hash suffixes const cols = Array.from(document.querySelectorAll('[class^="teams-detailed-leaderboard_columnContent__"]')); if (!cols.length) return false; let changed = false; cols.forEach((col) => { try { // find any label in the column that has a 2-5 digit rating const label = Array.from(col.querySelectorAll('label')).find(l => /\d{2,5}/.test((l.textContent || '').trim())); if (!label) return; const rating = extractFirstInteger(label.textContent || ''); if (rating == null) return; // get the nearest icon for this row/column const img = findNearestTeamIconFrom(col); if (!img || img.tagName !== 'IMG') return; // pick team-duel badge (pickForRating returns null if <1350) const badgeUrl = pickForRating(BADGES_TEAMDUEL, rating); if (!badgeUrl) return; // set original data attributes if not already set img.dataset.origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src') || img.getAttribute('src') || ''; img.dataset.origSrcset = img.dataset.origSrcset || img.getAttribute('data-orig-srcset') || img.getAttribute('srcset') || ''; const cur = img.getAttribute('src') || ''; if (!cur.includes(badgeUrl)) { img.setAttribute('src', badgeUrl); img.setAttribute('srcset', `${badgeUrl} 1x, ${badgeUrl} 2x`); img.dataset.replaced = 'true'; changed = true; console.log('[GG TeamsList] Replaced team list icon ->', badgeUrl, 'rating:', rating); } } catch (e) { // ignore individual row errors console.error('updateTeamListEntries item error', e); } }); return changed; } // -------------------- // Combined update + observer + fallback // -------------------- function updateAllOnce() { let changed = false; changed = updateDivisionArea() || changed; // pass divisionInfo to team badge updater const divisionInfo = getDivisionInfo(); changed = updateTeamBadges(divisionInfo) || changed; changed = updateMultiplayerAll() || changed; // new: update leaderboard / teams list icons changed = updateTeamListEntries() || changed; return changed; } // -------------------- // Debounce / schedule / observer (unchanged) // -------------------- let scheduled = null; function scheduleUpdate() { if (scheduled) return; scheduled = setTimeout(() => { scheduled = null; updateAllOnce(); }, 150); } const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) { scheduleUpdate(); break; } if (m.type === 'characterData') { scheduleUpdate(); break; } if (m.type === 'attributes') { scheduleUpdate(); break; } } }); function startObserving() { if (!document.body) return; observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true }); scheduleUpdate(); } // -------------------- // SPA route-change detection (pushState/replaceState/popstate) // Note: resetAll no longer restores images/titles per your request. // -------------------- (function installRouteWatcher() { const wrap = (method) => { const orig = history[method]; return function () { const rv = orig.apply(this, arguments); window.dispatchEvent(new Event('gg-route-change')); return rv; }; }; history.pushState = wrap('pushState'); history.replaceState = wrap('replaceState'); window.addEventListener('popstate', () => window.dispatchEvent(new Event('gg-route-change'))); // On route change: reset minimal state and re-run update window.addEventListener('gg-route-change', () => { try { resetAll(); } catch (e) { /* ignore */ } // small delay to let the new page render DOM setTimeout(updateAllOnce, 250); }); })(); // start if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', startObserving, { once: true }); } else startObserving(); const fallbackInterval = setInterval(() => updateAllOnce(), 5000); // cleanup window.addEventListener('beforeunload', () => { observer.disconnect(); clearInterval(fallbackInterval); if (scheduled) clearTimeout(scheduled); }); // initial run setTimeout(updateAllOnce, 300); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址