// ==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);
})();