您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Restores the old look of the gamecard to match its 2016 counterpart
// ==UserScript== // @name ROBLOX 2016 Gamecard Addon For RLOT // @namespace http://tampermonkey.net/ // @version 1.1 // @description Restores the old look of the gamecard to match its 2016 counterpart // @match *://www.roblox.com/* // @author The Noise! // @grant GM_addStyle // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAaJJREFUeJztmsFOAjEQhv/tildvJPpE8gQQTx6IvpnRJ1hj0OfRxIOGg0FY64mkIYRMu+1MGea7sYdO9+u0204BDMMwDMM4VRqpwK6b+vD33+RJpC9OIujl653ffbYrhAsRAR+/33ufS0gQEXCI8WLOKqE6AZ/rJWs8dgGUNL94uWXLguoyAACW/Q9brCoFcGICOIOdP9+IfOsPwSZgvJj7je+5wpFhE8D9eaNyVjqA1BaXii2CJRtP3dZyngyLCXDd1Nc670OKCKh93odUtwZwF0ayBxsy+hJVoawZcEypv4Vk/FhebNS0WF0/RGVRdWvAENYJW21VAoD4bFUnIBYTIN2BEsRMA5UCAKDtZiQJagV40JJArQCANhVUC6BsrVULoKBWAPVgpVJAzKlSpYAYTIB0B3ITW1RRJaBJKHCpEtBPHqMNqBHgEsubagRsEkYfUCQglZMXEL9oVFYhdmiS0x8YcDFSi4ihlynJU0Dqv70ho6Yd3EaWl2i7madWYHKSYxCyj+LV271/X33lbnYvVQrYUnqNSLkGMwzDMAzDCPkHg/Jw0+Nv/a8AAAAASUVORK5CYII= // @grant GM_xmlhttpRequest // @connect games.roblox.com // @connect api.roblox.com // @connect apis.roblox.com // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // Debug mode const DEBUG = true; function log(...args) { if (DEBUG) console.log('[Roblox 2016 Gamecard]', ...args); } function logError(...args) { console.error('[Roblox 2016 Gamecard ERROR]', ...args); } // Skip on certain favorites page const currentUrl = window.location.href; if (currentUrl.match(/roblox\.com\/users\/\d+\/favorites#!\/places/)) { log("Skipping execution on favorites places page"); return; } // Cache & rate limits const CACHE_VERSION = 1; const CACHE_EXPIRY = 24 * 60 * 60 * 1000; const API_DELAY = 2000; const MAX_RETRIES = 3; const RETRY_DELAY = 5000; let lastAPICall = 0; let pendingRequests = []; let isProcessingQueue = false; let gameDataCache = {}; let placeToUniverseCache = {}; let lastCacheSave = 0; const CACHE_SAVE_DELAY = 10000; // Page-type flags const isChartsPage = currentUrl.includes('roblox.com/charts'); const isGamesPage = currentUrl.includes('roblox.com/games/'); const isUsersPage = currentUrl.includes('roblox.com/users'); const isExactGamesUrl = /^https?:\/\/www\.roblox\.com\/games\/?(\?.*)?$/.test(currentUrl); log("Initializing on:", currentUrl); // Bail on unsupported pages if (currentUrl.includes('roblox.com/groups/') || currentUrl.includes('roblox.com/communities/')) { log("Skipping on unsupported page type"); return; } // Load cache function loadCache() { try { let gameCache = {}; const cachedData = JSON.parse(localStorage.getItem('roblox2016GamecardCache') || '{}'); if (cachedData.version === CACHE_VERSION && cachedData.data) { const now = Date.now(); for (const [id, item] of Object.entries(cachedData.data)) { if (now - item.timestamp < CACHE_EXPIRY) { gameCache[id] = item.data; } } log(`Loaded ${Object.keys(gameCache).length} games from cache`); } let mappingCache = {}; const pm = JSON.parse(localStorage.getItem('roblox2016PlaceToUniverseCache') || '{}'); if (pm.data) mappingCache = pm.data; log(`Loaded ${Object.keys(mappingCache).length} place→universe mappings`); return { gameCache, mappingCache }; } catch (e) { logError("Cache load failed", e); return { gameCache: {}, mappingCache: {} }; } } // Throttled save function saveCache() { const now = Date.now(); if (now - lastCacheSave < CACHE_SAVE_DELAY) return; lastCacheSave = now; try { const cacheObj = { version: CACHE_VERSION, timestamp: now, data: {} }; for (const [id, data] of Object.entries(gameDataCache)) { cacheObj.data[id] = { timestamp: now, data }; } localStorage.setItem('roblox2016GamecardCache', JSON.stringify(cacheObj)); localStorage.setItem('roblox2016PlaceToUniverseCache', JSON.stringify({ version: CACHE_VERSION, timestamp: now, data: placeToUniverseCache })); log(`Saved ${Object.keys(gameDataCache).length} games & ${Object.keys(placeToUniverseCache).length} mappings`); } catch (e) { logError("Cache save failed", e); } } // Extract IDs function getGameId(card) { try { const uId = findUniverseId(card); if (uId) return { universeId: uId, placeId: null }; const pId = findPlaceId(card); if (pId) { if (placeToUniverseCache[pId]) { return { universeId: placeToUniverseCache[pId], placeId: pId }; } return { universeId: null, placeId: pId }; } return { universeId: null, placeId: null }; } catch (e) { logError("getGameId error", e); return { universeId: null, placeId: null }; } } function findUniverseId(card) { if (card.dataset.universeId) return card.dataset.universeId; const link = card.querySelector('a.game-card-link[id]'); if (link && /^\d+$/.test(link.id)) return link.id; for (const a of card.querySelectorAll('a')) { const m = a.href.match(/[?&]universeId=(\d+)/); if (m) return m[1]; } return null; } function findPlaceId(card) { if (card.dataset.placeId) return card.dataset.placeId; if (isUsersPage) { const link = card.querySelector('a.game-card-link'); const m = link && link.href.match(/\/games\/(\d+)/); if (m) return m[1]; } for (const a of card.querySelectorAll('a')) { let m = a.href.match(/[?&]placeId=(\d+)/); if (m) return m[1]; m = a.href.match(/\/games\/(\d+)/); if (m) return m[1]; } return null; } // Convert placeId → universeId function convertPlaceIdToUniverseId(placeId, cb) { log("Converting placeId", placeId); GM_xmlhttpRequest({ method: "GET", url: `https://apis.roblox.com/universes/v1/places/${placeId}/universe`, headers: { Accept: "application/json" }, onload(res) { if (res.status === 200) { try { const d = JSON.parse(res.responseText); if (d.universeId) { placeToUniverseCache[placeId] = d.universeId; saveCache(); return cb(d.universeId); } } catch (e) { logError("Parse error", e); } } logError("Convert failed", res.status); cb(null); }, onerror(err) { logError("Convert error", err); cb(null); } }); } // Process a single card const processedCards = new Set(); function processCard(card, priority = 0) { if (processedCards.has(card)) return; processedCards.add(card); const { universeId, placeId } = getGameId(card); if (universeId) { const ext = createExtension(card, universeId); ext.dataset.priority = priority; if (gameDataCache[universeId]) { updateExtension(ext, gameDataCache[universeId]); } else { queueAPIRequest(universeId, ext, priority); } } else if (placeId) { const ext = createExtension(card, null); ext.dataset.priority = priority; ext.dataset.placeId = placeId; convertPlaceIdToUniverseId(placeId, uId => { if (uId) { ext.dataset.universeId = uId; if (gameDataCache[uId]) { updateExtension(ext, gameDataCache[uId]); } else { queueAPIRequest(uId, ext, priority); } } else { updateExtension(ext, { upVotes: 0, downVotes: 0, creatorName: "Unknown", creatorId: "1", creatorType: "user" }); } }); } } // Queue & fetch function queueAPIRequest(uId, ext, priority = 0, retryCount = 0) { pendingRequests.push({ universeId: uId, extension: ext, priority, retryCount, timestamp: Date.now() }); pendingRequests.sort((a, b) => b.priority - a.priority); if (!isProcessingQueue) processAPIQueue(); } function processAPIQueue() { if (!pendingRequests.length) { isProcessingQueue = false; return; } isProcessingQueue = true; const now = Date.now(); if (now - lastAPICall < API_DELAY) { return setTimeout(processAPIQueue, API_DELAY - (now - lastAPICall) + 100); } const req = pendingRequests.shift(); fetchGameData(req.universeId, req.extension, req.retryCount); } function fetchGameData(uId, ext, retryCount = 0) { lastAPICall = Date.now(); GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/votes?universeIds=${uId}`, headers: { Accept: "application/json" }, onload(res) { let up = 0, down = 0; if (res.status === 200) { try { const d = JSON.parse(res.responseText).data[0]; up = d.upVotes; down = d.downVotes; } catch (e) { logError("Votes parse", e); } } fetchCreatorInfo(uId, ext, up, down); setTimeout(processAPIQueue, API_DELAY); }, onerror() { fetchCreatorInfo(uId, ext, 0, 0); setTimeout(processAPIQueue, API_DELAY); } }); } function fetchCreatorInfo(uId, ext, up, down) { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games?universeIds=${uId}`, headers: { Accept: "application/json" }, onload(res) { let name = "ROBLOX", id = "1", type = "user"; if (res.status === 200) { try { const c = JSON.parse(res.responseText).data[0].creator; name = c.name; id = c.id; type = c.type.toLowerCase(); } catch (e) {} } const data = { upVotes: up, downVotes: down, creatorName: name, creatorId: id, creatorType: type }; gameDataCache[uId] = data; saveCache(); updateExtension(ext, data); }, onerror() { const data = { upVotes: up, downVotes: down, creatorName: "Unknown", creatorId: "1", creatorType: "user" }; gameDataCache[uId] = data; saveCache(); updateExtension(ext, data); } }); } // Helper to force the separator line on top function bringSeparatorToFront(ext) { const sep = ext.querySelector('.card-separator-line'); if (sep) sep.style.zIndex = '10000'; } // Build the hover extension function createExtension(card, universeId) { if (!card.dataset.cardId) { card.dataset.cardId = `card-${Math.floor(Math.random() * 1e6)}`; } const cid = card.dataset.cardId; let ext = document.getElementById(`extension-${cid}`); let shadow = document.getElementById(`shadow-${cid}`); if (ext && shadow) return ext; ext = document.createElement('div'); ext.className = 'card-extension'; ext.id = `extension-${cid}`; if (universeId) ext.dataset.universeId = universeId; ext.innerHTML = ` <div class="vote-up-count">...</div> <div class="vote-down-count">...</div> <div class="card-separator-line"></div> <div class="game-creator-container"> <span class="game-creator-by">By </span> <a class="game-creator-name" href="#">...</a> </div> `; shadow = document.createElement('div'); shadow.className = 'card-shadow'; shadow.id = `shadow-${cid}`; document.body.appendChild(ext); document.body.appendChild(shadow); function show() { ext.style.zIndex = '9999'; shadow.style.zIndex = '9998'; const r = card.getBoundingClientRect(); // move extension 2px higher (subtract 3 from bottom) ext.style.top = (r.bottom + window.scrollY - 3) + 'px'; ext.style.left = (r.left + window.scrollX) + 'px'; // fixed width: 146px on games pages (150 − 4), 150px elsewhere const width = isGamesPage ? 146 : 150; ext.style.width = width + 'px'; shadow.style.width = width + 'px'; shadow.style.top = (r.top + window.scrollY) + 'px'; shadow.style.left = (r.left + window.scrollX) + 'px'; shadow.style.height = (r.height + 44) + 'px'; bringSeparatorToFront(ext); ext.style.display = shadow.style.display = 'block'; } function hide() { ext.style.display = shadow.style.display = 'none'; } card.addEventListener('mouseenter', show); card.addEventListener('mouseleave', e => { if (![ext, shadow].includes(e.relatedTarget)) hide(); }); ext.addEventListener('mouseenter', show); ext.addEventListener('mouseleave', e => { if (![card, shadow].includes(e.relatedTarget)) hide(); }); shadow.addEventListener('mouseenter', show); shadow.addEventListener('mouseleave', e => { if (![card, ext].includes(e.relatedTarget)) hide(); }); return ext; } function updateExtension(ext, data) { try { ext.querySelector('.vote-up-count').textContent = data.upVotes.toLocaleString(); ext.querySelector('.vote-down-count').textContent = data.downVotes.toLocaleString(); const nameEl = ext.querySelector('.game-creator-name'); nameEl.textContent = data.creatorName; nameEl.href = data.creatorType === 'user' ? `https://www.roblox.com/users/${data.creatorId}/profile` : `https://www.roblox.com/groups/${data.creatorId}`; ext.classList.add('has-data'); } catch (e) { logError("updateExtension error", e); } } // Replace empty-vote spans with an empty bar function processEmptyVotes() { document.querySelectorAll('span.info-label.no-vote:not(.processed-empty-label)') .forEach(label => { label.classList.add('processed-empty-label'); const wrap = document.createElement('div'); wrap.style.display = 'inline-flex'; wrap.style.alignItems = 'center'; wrap.appendChild(createSegmentedBar(0)); wrap.appendChild(createThumbsDownIcon()); label.parentNode.replaceChild(wrap, label); }); } // Process percentage labels (1–100) const processedVoteLabels = new Set(); function processVoteLabels() { document.querySelectorAll('.info-label.vote-percentage-label:not(.processed-label)') .forEach(label => { processedVoteLabels.add(label); label.classList.add('processed-label'); const pct = parseInt(label.textContent, 10); if (isNaN(pct)) return; const wrap = document.createElement('div'); wrap.style.display = 'inline-flex'; wrap.style.alignItems = 'center'; wrap.appendChild(createSegmentedBar(pct)); wrap.appendChild(createThumbsDownIcon()); label.parentNode.replaceChild(wrap, label); }); } function createSegmentedBar(percent) { const segs = [19, 19, 19, 19, 21]; const totalFill = (percent / 100) * 97; let rem = totalFill; const cont = document.createElement('div'); cont.className = 'vote-bar-seg-container'; segs.forEach(w => { const seg = document.createElement('div'); seg.className = 'vote-segment'; seg.style.width = w + 'px'; const fill = document.createElement('div'); fill.className = 'vote-segment-filled'; const fw = Math.min(w, Math.max(0, rem)); fill.style.width = fw + 'px'; rem -= fw; seg.appendChild(fill); cont.appendChild(seg); }); return cont; } function createThumbsDownIcon() { const s = document.createElement('span'); s.className = 'vote-thumbs-down-icon'; return s; } // Process all cards function processAllCards() { processEmptyVotes(); processVoteLabels(); document.querySelectorAll('.game-sort-carousel-wrapper .game-card-container:not(.gamecard-processed)') .forEach(c => { c.classList.add('gamecard-processed'); processCard(c, 10); }); if (isUsersPage) { document.querySelectorAll('.game-card:not(.gamecard-processed), .hover-game-card:not(.gamecard-processed)') .forEach(c => { c.classList.add('gamecard-processed'); processCard(c, 5); }); } document.querySelectorAll('.game-card-container:not(.gamecard-processed)') .forEach(c => { c.classList.add('gamecard-processed'); processCard(c, 0); }); } // CSS styles GM_addStyle(` .vote-bar-seg-container { display:inline-block; width:105px; height:6px; vertical-align:middle; } .vote-segment { display:inline-block; height:100%; background:#b8b8b8; position:relative; vertical-align:middle; } .vote-segment:not(:last-child){margin-right:2px;} .vote-segment-filled { background:#757575; height:100%; width:0; } .vote-thumbs-down-icon { background-image:url("https://static.rbxcdn.com/images/Icons/thumbs.svg"); background-position:-16px -16px; background-repeat:no-repeat; background-size:32px; display:none; height:16px; width:16px; margin-left:0; position:relative; top:9px; filter:brightness(150%); } .game-card-container:hover .vote-segment, .game-card:hover .vote-segment { background:#eeadad !important; } .game-card-container:hover .vote-segment-filled, .game-card:hover .vote-segment-filled { background:#02b757 !important; } .game-card-container:hover .vote-thumbs-down-icon, .game-card:hover .vote-thumbs-down-icon { display:inline-block !important; } /* no longer hiding .no-vote spans; replaced via JS */ .card-extension { position:absolute; height:45px; background:#fff; border-bottom-left-radius:3px; border-bottom-right-radius:3px; display:none; z-index:9999 !important; box-shadow:none; pointer-events:auto; } .card-shadow { position:absolute; display:none; z-index:9998 !important; pointer-events:none; background:transparent; border-radius:3px; box-shadow:0 3px 6px rgba(0,0,0,0.4), 3px 0 6px -3px rgba(0,0,0,0.4), -3px 0 6px -3px rgba(0,0,0,0.4); } .vote-up-count { color:#02b757; font-size:12px!important; font-weight:300; opacity:0.6; position:absolute; left:7px; top:-5px; } .vote-down-count { color:rgb(226,118,118); font-size:12px!important; font-weight:300; opacity:0.6; position:absolute; right:7px; top:-5px; } .card-separator-line { position:absolute; height:1px; left:0; right:0; bottom:30px; background-color:#e3e3e3; z-index:10000 !important; } .game-creator-container { font-size:12px; font-weight:400; margin-left:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; width:calc(100% - 18px); position:absolute; bottom:5px; left:3px; } .game-creator-by { color:#b8b8b8; font-size:12px; } .game-creator-name { color:#00a2ff!important; text-decoration:none; font-size:12px; cursor:pointer; } .game-creator-name:hover { text-decoration:underline; } .game-card-container, .game-card { z-index:auto!important; } .game-card-container:hover, .game-card:hover { z-index:10!important; } `); // Init function initialize() { const { gameCache, mappingCache } = loadCache(); gameDataCache = gameCache; placeToUniverseCache = mappingCache; processAllCards(); new MutationObserver(muts => { if (muts.some(m => m.addedNodes.length)) processAllCards(); }).observe(document.body, { childList:true, subtree:true }); window.addEventListener('scroll', () => { clearTimeout(window._r2016_t); window._r2016_t = setTimeout(processAllCards, 500); }, { passive:true }); setInterval(saveCache, 30000); } setTimeout(initialize, 500); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址