ROBLOX 2016 Gamecard Addon For RLOT

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址