OC Role Display

Shows role positions with mobile scaling support

נכון ליום 06-03-2025. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         OC Role Display
// @namespace    http://tampermonkey.net/
// @version      1.0.4
// @description  Shows role positions with mobile scaling support
// @author       Allenone [2033011]
// @match        https://www.torn.com/factions.php?step=your*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM.xmlHttpRequest
// @grant        GM_info
// @connect      tornprobability.com
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';

    let roleData = null;
    let globalObserver = null;
    let processing = false;
    const debounceDelay = 200; // ms

    // Polyfill for requestIdleCallback for browsers that don't support it (common on mobile)
    const requestIdleCallback = window.requestIdleCallback || function(callback) {
        return setTimeout(callback, 200);
    };

    // Fetch role names from API
    try {
        roleData = await RoleNames();
    } catch (error) {
        console.error('Error fetching role data:', error);
        return;
    }

    function doOnHashChange() {
        if (processing) return;
        processing = true;

        requestIdleCallback(() => {
            try {
                // Use custom marker class "role-processed" instead of "processed" so as not to conflict with mobile styles
                const ocElements = document.querySelectorAll('.wrapper___U2Ap7:not(.role-processed)');
                if (ocElements.length === 0) return;

                for (let element of ocElements) {
                    element.classList.add('role-processed');
                    const reactFiberKey = Object.keys(element).find(key => key.startsWith("__reactFiber$"));
                    if (!reactFiberKey) continue;

                    const ocName = element.querySelector('.panelTitle___aoGuV')?.innerText || "Unknown";
                    const slots = element.querySelectorAll('.wrapper___Lpz_D');

                    for (let slot of slots) {
                        const slotFiberKey = Object.keys(slot).find(key => key.startsWith("__reactFiber$"));
                        if (!slotFiberKey) continue;

                        const roleName = slot.querySelector('.title___UqFNy');
                        if (!roleName) continue;

                        try {
                            const fiberNode = slot[slotFiberKey];
                            const key = fiberNode.return.key;
                            const roleText = getOCRoles(ocName, key);

                            if (roleText && roleName.innerText !== roleText) {
                                roleName.innerText = roleText;
                            }
                        } catch (error) {
                            console.error("Error accessing React properties:", error);
                        }
                    }
                }
            } finally {
                processing = false;
            }
        });
    }

    function getOCRoles(name, position) {
        if (!roleData) return;
        const ocRoles = roleData[name];
        if (!ocRoles) return;
        const positionKey = position.replace('slot-', '');
        return ocRoles[positionKey];
    }

    function observeButtonContainer() {
        let buttonContainer = document.querySelector('.buttonsContainer___aClaa');
        if (buttonContainer) {
            buttonContainer.addEventListener('click', () => {
                document.querySelectorAll('.wrapper___U2Ap7').forEach(el => {
                    el.classList.remove('role-processed');
                });
                doOnHashChange();
            });
        } else {
            setTimeout(observeButtonContainer, 500);
        }
    }

    function waitForElementsToReload(selector) {
        return new Promise(resolve => {
            const tempObserver = new MutationObserver(() => {
                if (document.querySelector(selector)) {
                    tempObserver.disconnect();
                    resolve();
                }
            });

            tempObserver.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function setupHashChangeListener() {
        window.addEventListener('hashchange', () => {
            document.querySelectorAll('.wrapper___U2Ap7.role-processed').forEach(el => {
                el.classList.remove('role-processed');
            });
            doOnHashChange();
        });
    }

    function initializeScript() {
        if (globalObserver) globalObserver.disconnect();

        const targetNode = document.querySelector('#factionCrimes-root') || document.body;

        globalObserver = new MutationObserver(debounce(() => {
            if (!document.querySelector('.wrapper___U2Ap7')) return;
            doOnHashChange();
        }, debounceDelay));

        globalObserver.observe(targetNode, {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: false
        });

        doOnHashChange();
        observeButtonContainer();
        setupHashChangeListener();
    }

    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    function RoleNames() {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: 'GET',
                url: 'https://tornprobability.com:3000/GetRoleNames',
                headers: { 'Content-Type': 'application/json' },
                onload: (response) => {
                    try {
                        resolve(JSON.parse(response.responseText));
                    } catch (err) {
                        reject(err);
                    }
                },
                onerror: (err) => reject(err)
            });
        });
    }

    // Initialize script
    if (document.readyState === 'complete') {
        initializeScript();
    } else {
        window.addEventListener('load', initializeScript);
    }
})();