您需要先安装一个扩展,例如 篡改猴、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.0 // @description Restores the old look of the gamecard to match its 2016 counterpart // @match *://www.roblox.com/* // @author The Noise! [With some help of cursor ai!] // @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); } // Check if URL matches the exclusion pattern const currentUrl = window.location.href; // Check if the URL is a user favorites places page that we want to exclude if (currentUrl.match(/roblox\.com\/users\/\d+\/favorites#!\/places/)) { log("Skipping execution on favorites places page"); return; } // Cache configuration const CACHE_VERSION = 1; const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds // Rate limiting settings const API_DELAY = 2000; // 2 seconds between API calls const MAX_RETRIES = 3; const RETRY_DELAY = 5000; // 5 seconds initial retry delay let lastAPICall = 0; let pendingRequests = []; let isProcessingQueue = false; // Game data cache let gameDataCache = {}; // Additional cache for placeId to universeId mappings let placeToUniverseCache = {}; let lastCacheSave = 0; const CACHE_SAVE_DELAY = 10000; // 10 seconds between cache writes // Check page type const isChartsPage = currentUrl.includes('roblox.com/charts'); const isGamesPage = currentUrl.includes('roblox.com/games/'); const isUsersPage = currentUrl.includes('roblox.com/users'); const isExactGamesUrl = currentUrl.match(/^https?:\/\/www\.roblox\.com\/games\/?(\?.*)?$/); log("Initializing on page:", currentUrl, "isExactGamesUrl:", !!isExactGamesUrl, "isUsersPage:", isUsersPage); // Skip on some pages if (currentUrl.includes('roblox.com/groups/') || currentUrl.includes('roblox.com/communities/')) { log("Skipping on unsupported page type"); return; } // Load cache from localStorage function loadCache() { try { // Load game data cache const cachedData = localStorage.getItem('roblox2016GamecardCache'); let gameCache = {}; if (cachedData) { const cacheObject = JSON.parse(cachedData); // Check cache version if (cacheObject.version !== CACHE_VERSION) { log("Cache version mismatch, resetting cache"); } else { // Check expiry and filter out expired items const now = Date.now(); let expiredCount = 0; Object.entries(cacheObject.data).forEach(([id, item]) => { if (now - item.timestamp < CACHE_EXPIRY) { gameCache[id] = item.data; } else { expiredCount++; } }); log(`Loaded ${Object.keys(gameCache).length} cached game items, ${expiredCount} expired items removed`); } } // Load placeId to universeId mapping cache const placeMappingCache = localStorage.getItem('roblox2016PlaceToUniverseCache'); let mappingCache = {}; if (placeMappingCache) { try { const mappingObject = JSON.parse(placeMappingCache); mappingCache = mappingObject.data || {}; log(`Loaded ${Object.keys(mappingCache).length} place-to-universe mappings`); } catch (e) { logError("Error parsing place mapping cache", e); } } return { gameCache, mappingCache }; } catch (e) { logError("Failed to load cache from localStorage", e); return { gameCache: {}, mappingCache: {} }; } } // Save cache to localStorage with throttling function saveCache() { const now = Date.now(); if (now - lastCacheSave < CACHE_SAVE_DELAY) return; lastCacheSave = now; try { // Save game data cache const cacheObject = { version: CACHE_VERSION, timestamp: now, data: {} }; Object.entries(gameDataCache).forEach(([id, data]) => { cacheObject.data[id] = { timestamp: now, data: data }; }); localStorage.setItem('roblox2016GamecardCache', JSON.stringify(cacheObject)); // Save placeId mapping cache const mappingObject = { version: CACHE_VERSION, timestamp: now, data: placeToUniverseCache }; localStorage.setItem('roblox2016PlaceToUniverseCache', JSON.stringify(mappingObject)); log(`Saved ${Object.keys(gameDataCache).length} game items and ${Object.keys(placeToUniverseCache).length} mappings to cache`); } catch (e) { logError("Failed to save cache to localStorage", e); } } // Helper function to get a placeId or universeId from the card function getGameId(card) { try { // First try to find universeId (preferable) const universeId = findUniverseId(card); if (universeId) { return { universeId, placeId: null }; } // If no universeId found, try to find placeId instead const placeId = findPlaceId(card); if (placeId) { // Check if we already have the mapping in cache if (placeToUniverseCache[placeId]) { return { universeId: placeToUniverseCache[placeId], placeId }; } return { universeId: null, placeId }; } return { universeId: null, placeId: null }; } catch (e) { logError("Error extracting game ID", e); return { universeId: null, placeId: null }; } } // Look for universeId function findUniverseId(card) { // Try data-universe-id attribute if (card.dataset.universeId) { return card.dataset.universeId; } // Try game-card-link ID const gameLink = card.querySelector('a.game-card-link[id]'); if (gameLink && gameLink.id && /^\d+$/.test(gameLink.id)) { if (gameLink.id.length > 5 && gameLink.id.length < 12) { return gameLink.id; } } // Try universeId parameter in URL const allLinks = card.querySelectorAll('a'); for (const link of allLinks) { const href = link.getAttribute('href'); if (!href) continue; const universeIdMatch = href.match(/[?&]universeId=(\d+)/i); if (universeIdMatch && universeIdMatch[1]) { return universeIdMatch[1]; } } return null; } // Look for placeId function findPlaceId(card) { // Try data-place-id attribute if (card.dataset.placeId) { return card.dataset.placeId; } // Special case for user pages - extract from game link URL if (isUsersPage) { const gameLink = card.querySelector('a.game-card-link'); if (gameLink && gameLink.href) { const match = gameLink.href.match(/\/games\/(\d+)/i); if (match && match[1] && match[1].length > 5 && match[1].length < 12) { return match[1]; } } } // Try placeId parameter in URL const allLinks = card.querySelectorAll('a'); for (const link of allLinks) { const href = link.getAttribute('href'); if (!href) continue; // Look for placeId parameter const placeIdMatch = href.match(/[?&]placeId=(\d+)/i); if (placeIdMatch && placeIdMatch[1]) { return placeIdMatch[1]; } // Look for /games/ID/ pattern (this is often the placeId) const gamesMatch = href.match(/\/games\/(\d+)/i); if (gamesMatch && gamesMatch[1]) { return gamesMatch[1]; } } return null; } // Convert placeId to universeId function convertPlaceIdToUniverseId(placeId, callback) { log("Converting placeId to universeId:", placeId); const url = `https://apis.roblox.com/universes/v1/places/${placeId}/universe`; GM_xmlhttpRequest({ method: "GET", url: url, headers: {"Accept": "application/json"}, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data && data.universeId) { log(`Converted placeId ${placeId} to universeId ${data.universeId}`); // Save to mapping cache placeToUniverseCache[placeId] = data.universeId; saveCache(); callback(data.universeId); return; } } catch (e) { logError("Error parsing universe conversion response", e); } } logError("Failed to convert placeId to universeId", response.status, response.responseText.substring(0, 100)); callback(null); // Signal failure }, onerror: function(error) { logError("Universe conversion request error", error); callback(null); // Signal failure } }); } const processedCards = new Set(); const processedVoteLabels = new Set(); const processedEmptyLabels = new Set(); // Remove empty no-vote labels function removeEmptyVoteLabels() { const emptyLabels = document.querySelectorAll('span.info-label.no-vote:not(.processed-empty-label)'); if (emptyLabels.length === 0) return; log(`Removing ${emptyLabels.length} empty vote labels`); emptyLabels.forEach(label => { if (processedEmptyLabels.has(label)) return; processedEmptyLabels.add(label); label.classList.add('processed-empty-label'); // Either remove or hide it if (label.textContent.trim() === '') { label.style.display = 'none'; // Hide it // Alternative: label.remove(); // Remove it completely } }); } // Queue system for API requests with priority function queueAPIRequest(universeId, extension, priority = 0, retryCount = 0) { pendingRequests.push({ universeId, extension, retryCount, priority, timestamp: Date.now() }); // Sort by priority (higher number = higher priority) pendingRequests.sort((a, b) => b.priority - a.priority); if (!isProcessingQueue) { processAPIQueue(); } } function processAPIQueue() { if (pendingRequests.length === 0) { isProcessingQueue = false; return; } isProcessingQueue = true; const now = Date.now(); // Enforce delay between API calls if (now - lastAPICall < API_DELAY) { setTimeout(processAPIQueue, API_DELAY - (now - lastAPICall) + 100); return; } // Get the next request const request = pendingRequests.shift(); fetchGameData(request.universeId, request.extension, request.retryCount); } // Process a single game card function processCard(card, priority = 0) { if (processedCards.has(card)) return; processedCards.add(card); const gameIds = getGameId(card); if (gameIds.universeId) { // We have the universeId directly, proceed as normal const extension = createExtension(card, gameIds.universeId); if (!extension) return; extension.dataset.priority = priority.toString(); if (gameDataCache[gameIds.universeId]) { log("Using cached data for universeId:", gameIds.universeId); updateExtension(extension, gameDataCache[gameIds.universeId]); } else { queueAPIRequest(gameIds.universeId, extension, priority); } } else if (gameIds.placeId) { // Need to convert placeId to universeId first const extension = createExtension(card, null); if (!extension) return; extension.dataset.priority = priority.toString(); extension.dataset.placeId = gameIds.placeId; convertPlaceIdToUniverseId(gameIds.placeId, function(universeId) { if (universeId) { extension.dataset.universeId = universeId; if (gameDataCache[universeId]) { log("Using cached data for converted universeId:", universeId); updateExtension(extension, gameDataCache[universeId]); } else { queueAPIRequest(universeId, extension, priority); } } else { // Fallback - show an error state updateExtension(extension, { upVotes: 0, downVotes: 0, creatorName: "Unknown", creatorId: "1", creatorType: "user" }); } }); } else { logError("Could not find any usable ID for card", card); } } // Create extension element function createExtension(card, universeId) { try { // Generate unique ID for this card if (!card.dataset.cardId) { card.dataset.cardId = `card-${Math.floor(Math.random() * 1000000)}`; } // Create extension and shadow elements const cardId = card.dataset.cardId; let extension = document.getElementById(`extension-${cardId}`); let shadow = document.getElementById(`shadow-${cardId}`); if (extension && shadow) { return extension; } // Create if not exists extension = document.createElement('div'); extension.className = 'card-extension'; extension.id = `extension-${cardId}`; if (universeId) { extension.dataset.universeId = universeId; } extension.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-${cardId}`; document.body.appendChild(extension); document.body.appendChild(shadow); // Function to show/hide extension const showExtension = () => { const rect = card.getBoundingClientRect(); extension.style.top = (rect.bottom + window.scrollY - 1) + 'px'; extension.style.left = rect.left + window.scrollX + 'px'; // Width calculations - special case for exact /games URL if (isExactGamesUrl) { // Special case: remove 4px from right side ONLY on /games extension.style.width = (rect.width - 15) + 'px'; shadow.style.width = (rect.width - 15) + 'px'; } else if (isChartsPage) { // Special case: fixed width of 150px for charts page extension.style.width = '150px'; shadow.style.width = '150px'; } else if (isUsersPage) { // Special case: fixed width of 150px for user pages extension.style.width = '150px'; shadow.style.width = '150px'; } else if (isGamesPage) { extension.style.width = (rect.width - 11) + 'px'; shadow.style.width = (rect.width - 11) + 'px'; } else { extension.style.width = (rect.width - 11) + 'px'; shadow.style.width = (rect.width - 11) + 'px'; } shadow.style.top = rect.top + window.scrollY + 'px'; shadow.style.left = rect.left + window.scrollX + 'px'; shadow.style.height = (rect.height + 45 - 1) + 'px'; extension.style.display = 'block'; shadow.style.display = 'block'; }; const hideExtension = () => { extension.style.display = 'none'; shadow.style.display = 'none'; }; // Add hover events for the card card.addEventListener('mouseenter', showExtension); card.addEventListener('mouseleave', (e) => { // Only hide if not entering the extension or shadow if (e.relatedTarget !== extension && e.relatedTarget !== shadow) { hideExtension(); } }); // Add hover events for the extension itself extension.addEventListener('mouseenter', showExtension); extension.addEventListener('mouseleave', (e) => { // Only hide if not entering the card or shadow if (e.relatedTarget !== card && e.relatedTarget !== shadow) { hideExtension(); } }); // Add hover events for the shadow shadow.addEventListener('mouseenter', showExtension); shadow.addEventListener('mouseleave', (e) => { // Only hide if not entering the card or extension if (e.relatedTarget !== card && e.relatedTarget !== extension) { hideExtension(); } }); return extension; } catch (e) { logError("Error creating extension", e); return null; } } // Fetch game data using the API function fetchGameData(universeId, extension, retryCount = 0) { log("Fetching data for universeId:", universeId, "Retry:", retryCount); lastAPICall = Date.now(); // Fetch votes first const votesUrl = `https://games.roblox.com/v1/games/votes?universeIds=${universeId}`; GM_xmlhttpRequest({ method: "GET", url: votesUrl, headers: {"Accept": "application/json"}, onload: function(response) { log(`Votes API response (${universeId}):`, response.status); let upVotes = 0, downVotes = 0; if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data && data.data && data.data[0]) { upVotes = data.data[0].upVotes || 0; downVotes = data.data[0].downVotes || 0; log(`Votes for ${universeId}:`, upVotes, downVotes); } } catch (e) { logError("Error parsing votes response", e); } // Only continue with game info if votes succeeded fetchCreatorInfo(universeId, extension, upVotes, downVotes, retryCount); } else if (response.status === 429 && retryCount < MAX_RETRIES) { // Rate limited - queue for retry with exponential backoff const delayTime = RETRY_DELAY * Math.pow(2, retryCount); log(`Rate limited. Retrying in ${delayTime/1000} seconds...`); setTimeout(() => { // Use the original priority when re-queuing const extensionElement = document.getElementById(extension.id); const priority = extensionElement && extensionElement.dataset.priority ? parseInt(extensionElement.dataset.priority) : 0; queueAPIRequest(universeId, extension, priority, retryCount + 1); }, delayTime); } else { logError("Votes API error", response.status, response.responseText.substring(0, 100)); // Try to get at least creator info fetchCreatorInfo(universeId, extension, 0, 0, retryCount); } // Continue processing the queue setTimeout(processAPIQueue, API_DELAY); }, onerror: function(error) { logError("Votes request error", error); if (retryCount < MAX_RETRIES) { // Queue for retry with exponential backoff const delayTime = RETRY_DELAY * Math.pow(2, retryCount); setTimeout(() => { // Use the original priority when re-queuing const extensionElement = document.getElementById(extension.id); const priority = extensionElement && extensionElement.dataset.priority ? parseInt(extensionElement.dataset.priority) : 0; queueAPIRequest(universeId, extension, priority, retryCount + 1); }, delayTime); } else { // Max retries reached, try getting at least creator info fetchCreatorInfo(universeId, extension, 0, 0, 0); } // Continue processing the queue setTimeout(processAPIQueue, API_DELAY); } }); } // Fetch just creator info function fetchCreatorInfo(universeId, extension, upVotes, downVotes, retryCount = 0) { const gameInfoUrl = `https://games.roblox.com/v1/games?universeIds=${universeId}`; GM_xmlhttpRequest({ method: "GET", url: gameInfoUrl, headers: {"Accept": "application/json"}, onload: function(infoResponse) { log(`Game info API response (${universeId}):`, infoResponse.status); let creatorName = "ROBLOX"; let creatorId = "1"; let creatorType = "user"; if (infoResponse.status === 200) { try { const infoData = JSON.parse(infoResponse.responseText); if (infoData && infoData.data && infoData.data[0] && infoData.data[0].creator) { creatorName = infoData.data[0].creator.name; creatorId = infoData.data[0].creator.id; creatorType = infoData.data[0].creator.type.toLowerCase(); log(`Creator for ${universeId}:`, creatorName, creatorId, creatorType); } } catch (e) { logError("Error parsing game info response", e); } // Store data in cache const gameData = { upVotes, downVotes, creatorName, creatorId, creatorType }; gameDataCache[universeId] = gameData; // Save to persistent cache saveCache(); // Update UI updateExtension(extension, gameData); } else if (infoResponse.status === 429 && retryCount < MAX_RETRIES) { // Rate limited - use what we have so far and store it const gameData = { upVotes, downVotes, creatorName: "Loading...", creatorId: "1", creatorType: "user" }; gameDataCache[universeId] = gameData; updateExtension(extension, gameData); // Queue for retry with exponential backoff const delayTime = RETRY_DELAY * Math.pow(2, retryCount); log(`Rate limited (creator). Retrying in ${delayTime/1000} seconds...`); setTimeout(() => { // Use the original priority when re-queuing const extensionElement = document.getElementById(extension.id); const priority = extensionElement && extensionElement.dataset.priority ? parseInt(extensionElement.dataset.priority) : 0; queueAPIRequest(universeId, extension, priority, retryCount + 1); }, delayTime); } else { logError("Game info API error", infoResponse.status, infoResponse.responseText.substring(0, 100)); // Use what we have const gameData = { upVotes, downVotes, creatorName: "Unknown", creatorId: "1", creatorType: "user" }; gameDataCache[universeId] = gameData; saveCache(); updateExtension(extension, gameData); } }, onerror: function(error) { logError("Game info request error", error); // Use what we have const gameData = { upVotes, downVotes, creatorName: "Error", creatorId: "1", creatorType: "user" }; gameDataCache[universeId] = gameData; saveCache(); updateExtension(extension, gameData); } }); } // Update extension with data function updateExtension(extension, gameData) { if (!extension) return; try { const upVotes = extension.querySelector('.vote-up-count'); const downVotes = extension.querySelector('.vote-down-count'); const creatorName = extension.querySelector('.game-creator-name'); if (upVotes) upVotes.textContent = Number(gameData.upVotes).toLocaleString(); if (downVotes) downVotes.textContent = Number(gameData.downVotes).toLocaleString(); if (creatorName) { creatorName.textContent = gameData.creatorName; if (gameData.creatorType === 'user') { creatorName.href = `https://www.roblox.com/users/${gameData.creatorId}/profile`; } else { creatorName.href = `https://www.roblox.com/groups/${gameData.creatorId}`; } } extension.classList.add('has-data'); } catch (e) { logError("Error updating extension", e); } } // Create segmented vote bar function createSegmentedBar(percent) { try { const segmentWidths = [19, 19, 19, 19, 21]; const totalFillableWidth = 97; const fillPixelsTotal = (percent / 100) * totalFillableWidth; let remainingFill = fillPixelsTotal; // Use document fragment for better performance const fragment = document.createDocumentFragment(); const container = document.createElement('div'); container.className = 'vote-bar-seg-container'; segmentWidths.forEach(width => { const segment = document.createElement('div'); segment.className = 'vote-segment'; segment.style.width = width + 'px'; const fillDiv = document.createElement('div'); fillDiv.className = 'vote-segment-filled'; const fillWidth = Math.min(width, Math.max(0, remainingFill)); fillDiv.style.width = fillWidth + 'px'; remainingFill -= fillWidth; segment.appendChild(fillDiv); container.appendChild(segment); }); fragment.appendChild(container); return fragment; } catch (e) { logError("Error creating segmented bar:", e); return document.createDocumentFragment(); } } // Create thumbs down icon function createThumbsDownIcon() { const icon = document.createElement('span'); icon.className = 'vote-thumbs-down-icon'; return icon; } // Process vote labels function processVoteLabels() { const voteLabels = document.querySelectorAll('.info-label.vote-percentage-label:not(.processed-label)'); if (voteLabels.length === 0) return; log(`Processing ${voteLabels.length} vote labels`); voteLabels.forEach(label => { if (processedVoteLabels.has(label)) return; processedVoteLabels.add(label); label.classList.add('processed-label'); try { const text = label.textContent.trim(); const percentValue = parseInt(text.replace('%', ''), 10); if (isNaN(percentValue)) return; const wrapper = document.createElement('div'); wrapper.style.display = 'inline-flex'; wrapper.style.alignItems = 'center'; wrapper.appendChild(createSegmentedBar(percentValue)); wrapper.appendChild(createThumbsDownIcon()); label.parentNode.replaceChild(wrapper, label); } catch (e) { logError("Error processing vote label", e); } }); } // Process all game cards with priority for carousel cards function processAllCards() { // First process and hide empty vote labels removeEmptyVoteLabels(); // Then prioritize cards in carousels as requested const carouselCards = document.querySelectorAll('.game-sort-carousel-wrapper .game-card-container:not(.gamecard-processed)'); log(`Processing ${carouselCards.length} carousel game cards with HIGH priority`); carouselCards.forEach(card => { card.classList.add('gamecard-processed'); // Process carousel cards with priority 10 (high) processCard(card, 10); }); // Special handling for user pages - they need different card selectors if (isUsersPage) { // Find game cards on user pages const userPageCards = document.querySelectorAll('.game-card:not(.gamecard-processed), .hover-game-card:not(.gamecard-processed)'); log(`Processing ${userPageCards.length} user page game cards`); userPageCards.forEach(card => { card.classList.add('gamecard-processed'); processCard(card, 5); // Medium priority }); } // Then process regular cards const regularCards = document.querySelectorAll('.game-card-container:not(.gamecard-processed)'); log(`Processing ${regularCards.length} regular game cards`); regularCards.forEach(card => { card.classList.add('gamecard-processed'); // Process regular cards with priority 0 (normal) processCard(card, 0); }); // Also process vote bars processVoteLabels(); } // Apply CSS styles GM_addStyle(` /* Vote bar styles */ .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-x: -16px; background-position-y: -16px; background-repeat: no-repeat; background-size: 32px; box-sizing: border-box; cursor: pointer; display: none; height: 16px; width: 16px; margin-left: 0px; position: relative; top: 9px; filter: brightness(150%); } .game-card-container:hover .vote-bar-seg-container .vote-segment, .game-card:hover .vote-bar-seg-container .vote-segment { background: #eeadad !important; } .game-card-container:hover .vote-bar-seg-container .vote-segment-filled, .game-card:hover .vote-bar-seg-container .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; } /* Hide empty no-vote labels */ span.info-label.no-vote { display: none !important; } /* Extension styling */ .card-extension { position: absolute; height: 45px; background-color: #ffffff; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; display: none; z-index: 1000; box-shadow: none; } /* Shadow element */ .card-shadow { position: absolute; display: none; z-index: 999; 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); } /* Like counter styling */ .vote-up-count { color: #02b757; font-size: 12px !important; font-weight: 300; opacity: 0.6; position: absolute; left: 7px; top: -5px; } /* Dislike counter styling */ .vote-down-count { color: rgb(226, 118, 118); font-size: 12px !important; font-weight: 300; opacity: 0.6; position: absolute; right: 7px; top: -5px; } /* Separator line */ .card-separator-line { position: absolute; height: 1px; width: 150px; background-color: #e3e3e3; bottom: 30px; left: 0px; } /* Creator container */ .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; } /* "By" text */ .game-creator-by { color: #b8b8b8; font-weight: 400; font-size: 12px; } /* Creator name */ .game-creator-name { color: #00a2ff !important; text-decoration: none; font-size: 12px; font-weight: 400; cursor: pointer; } /* Creator name hover */ .game-creator-name:hover { text-decoration: underline; } /* Game cards hover */ .game-card-container, .game-card { z-index: auto !important; } .game-card-container:hover, .game-card:hover { z-index: 10 !important; } /* Fix for extension and shadow being part of hover logic */ .card-extension { pointer-events: auto; } `); // Initialize function initialize() { log("Initializing script"); // Load cache from localStorage first const { gameCache, mappingCache } = loadCache(); gameDataCache = gameCache; placeToUniverseCache = mappingCache; // Process cards processAllCards(); // Add observer for new cards const observer = new MutationObserver(mutations => { let needsUpdate = false; mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { needsUpdate = true; } }); if (needsUpdate) { processAllCards(); } }); observer.observe(document.body, { childList: true, subtree: true }); // Process cards on scroll, but throttle it let scrollTimeout; window.addEventListener('scroll', () => { if (scrollTimeout) clearTimeout(scrollTimeout); scrollTimeout = setTimeout(processAllCards, 500); }, { passive: true }); // Periodically save cache setInterval(saveCache, 30000); log("Initialization complete"); } // Start the script with a delay to let the page load setTimeout(initialize, 500); })();