CE autoSig development

Add dynamic content to your profile signature

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         CE autoSig development
// @namespace    Cartel Empire
// @version      2025-07-26
// @description  Add dynamic content to your profile signature
// @author       Marlis[15746]
// @match        https://cartelempire.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=cartelempire.online
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

class DataCollector{
    /** @type {object} pages - Contains data about how data is collected */
    pages;

    /**
     * Create a DataCollector object
     *
     * @param {object} pages - Potentially pre-written page data
     */
    constructor(pages = {}){ //3.6e6 = 1 hour in ms
        this.pages = pages;
    }

    /**
     * Register a collection element that collects data based on its parameters
     *
     * @param {string} id - A unique id for the added collection element
     * @param {RegExp} regex - The regular expression to match the url against. If it matches, the handler is executed
     * @param {function} handler - The function that collects the data
     * @param {object} dataFormat - The default object to be stored when no data is present
     * @param {number} [updateInterval=3.6e6] - The minimum amount of time in milliseconds between updating the data
     */
    addPage(id, regex, handler, dataFormat, updateInterval=3.6e6){
        this.pages[id] = {regex: regex, handler: handler, dataFormat: dataFormat, updateInterval: updateInterval};
    }

    /**
     * Check each collection element for matching regexes and if a match is found, execute the element's handler
     *
     * @param {string} croppedURL - the cropped url of the current page
     */
    execute(croppedURL){
        Object.entries(this.pages).forEach(e => {
            if(e[1].regex.test(croppedURL)){
                const existingData = this.getStoredData(e[0], e[1].dataFormat);

                if(Date.now() - existingData.last_updated > e[1].updateInterval){
                    const newData = e[1].handler(croppedURL);
                    this.setStoredData(e[0], newData);
                }
            }
        });
    }

    /**
     * Store given data in tampermonkey's persistent storage
     *
     * @param {string} id - the collection element id of which the data will be stored
     * @param {object} data - the data to be stored
     *
     * @return {undefined} If no data is given, return before storing the (empty) data
     */
    setStoredData(id, data){
        if(!data){
            console.warn(`Attempt to write empty object to ${id} storage`);
            return;
        }
        data.last_updated = Date.now();
        GM_setValue(id, data);
        console.log(`Updated ${id} data`);
    }

    /**
     * Get stored data from tampermonkey's persistent storage
     *
     * @param {string} id - the collection element id of which the data is stored of
     * @param {object} format - the default format of the collection element
     *
     * @return {object} The stored data or the default format, if no data is stored
     */
    getStoredData(id, format){
        const data = GM_getValue(id);
        if(!data){
            format.last_updated = 0;
            GM_setValue(id, format);
        }
        return data || format;
    }
}

class SignatureConstructor{
    /** @type {template} template - A template element to build the signature on */
    template;
    /** @type {array<objects>} signatures - Data to construct the signatures with */
    signatures;

    /**
     * Create a signature object
     *
     * @param {template} template - The template to build the signature on
     * @param {signatures} [signatures=[]] - The data for signature construction
     */
    constructor(template, signatures = []){
        this.template = template;
        this.signatures = signatures;
    }

    /**
     * Register a signature construction element
     *
     * @param {string} elemId - The id of the html-element in which to insert the signature
     * @param {function} signatureConstructor - The handler to construct the signature
     * @param {...string} dataIds - The ids of the collection elements, of which the data can be used in signatureConstructor
     */
    addSignature(elemId, signatureConstructor, ...dataIds){
        this.signatures.push({id: elemId, handler: signatureConstructor, dataIds: dataIds});
    }

    /**
     * Construct the complete signature, using each of the signature construction elements stored in "signatures"
     *
     * @return {string} The complete constructed signature as a string
     */
    constructSignature(){
        const content = this.template.content;

        this.signatures.forEach((e, i) => {
            const tab = content.querySelector("#" + e.id + ".autoSig");
            const data = e.dataIds.map(e => GM_getValue(e));
            if(tab && data.every(e => e)){
                tab.innerHTML = e.handler(...data);
            } else{
                console.warn(`Could not construct profile signature for ${e.id}`);
            }
        });
        return this.template.innerHTML.replaceAll('\n', '');
    }
}

(function() {
    'use strict';

    const dataCollector = new DataCollector();
    dataCollector.addPage("jobs", /^jobs\/?$/, inJobs, {percentages: [], prestiges: []});
    dataCollector.addPage("stats", /^user\/stats\/?$/, inStats, {attempts: [], successes: []});
    dataCollector.addPage("profileSettings", /^settings/, inSettings, {}, 0);

    const URL = window.location.href.split(/\/|\?/g).slice(3).join('/').replace(/#[^\?\/]*$/, "").toLowerCase() || "home";
    dataCollector.execute(URL);

})();

/**
 * Collect job data on the job page
 *
 * @param {string} url - The cropped url that matches the collection element's regex
 *
 * @return {object} The collected data, should follow the collection element's dataFormat
 */
function inJobs(url) {
    const jobPanels = document.querySelectorAll("div.equipmentModule div.flex-column");
    const bars = document.querySelectorAll("div.equipmentModule .progress-bar");
    if(jobPanels === null) return;

    const prestiges = [];
    const percentages = [];

    for(const i in [...jobPanels]) {
        const jobPanel = jobPanels[i];
        const bar = jobPanel.querySelector(".progress-bar");
        const val = parseFloat(bar.getAttribute("aria-valuenow"));
        percentages.push(parseFloat(val.toFixed(2)));

        let prestige = jobPanel.querySelector(".bi.bi-star-fill.align-baseline")
        prestige = prestige ? prestige.nextSibling.innerText : "x0";
        prestige = parseInt(prestige.slice(1));
        prestiges.push(prestige);
    }
    return {percentages: percentages, prestiges: prestiges};
}

/**
 * Collect job data on the stats page
 *
 * @param {string} url - The cropped url that matches the collection element's regex
 *
 * @return {object} The collected data, should follow the collection element's dataFormat
 */
function inStats(url) {
    const attemptList = new Array(10);
    const successList = new Array(10);
    //for some reason the order of jobs is different here than everywhere else
    const indexMap = [0, 1, 2, 3, 8, 9, 4, 5, 6, 7];
    const statList = document.querySelectorAll("#mainBackground > div > div > div.col-12 > div.mb-4.card > div.card-body > div > ul:nth-of-type(4) > li > .row > .col-4:nth-child(3)");
    for(const i in [...statList]){
        if(i % 2 === 1) attemptList[indexMap[(i-1)/2]] = parseInt(statList[i].textContent.replaceAll(',', ''));
        else if(i != 0) successList[indexMap[(i/2)-1]] = parseInt(statList[i].textContent.replaceAll(',', ''));
    }

    return {attempts: attemptList, successes: successList};
}

/**
 * Put the constructed profile signature into the tinyMCE editor that edits the profile signature
 *
 * @param {string} url - The cropped url that matches the collection element's regex
 */
function inSettings(url) {
    const profileBtn = document.querySelector("#v-tab-profile");
    const evtListener = profileBtn.addEventListener("click", e => {
        const template = document.createElement("template");
        const editor = tinymce.get("profileSignatureEditor")
        const textSig = editor.getContent().replaceAll('\n', '');

        if(!tinymce || !textSig) return;
        template.innerHTML = textSig;

        const sigConstructor = new SignatureConstructor(template);
        sigConstructor.addSignature("jobs", constructJobSig, "jobs", "stats");

        const updatedSignature = sigConstructor.constructSignature();

        editor.setContent(updatedSignature);
    }, {once: true});
}

/**
 * Construct the profile signature for the job tab
 *
 * @param {object} jobs - The "job" collection element data
 * @param {object} stats - The "stats" collection element data
 *
 * @return {string} The constructed job signature
 */
function constructJobSig(jobs, stats){
    const jobNames = ["Intimidation", "Arson", "GTA", "Drug Transport", "Farm Robbery", "Agave Robbery", "Paste Robbery", "Construction Robbery", "Blackmail", "Hacking"];

    const prestHSL = jobs.prestiges.map(e => (e/10)*120); // [0, 10] prestige HSL
    const percHSL = jobs.percentages.map(e => Math.floor((e/100)*120)); // [0, 100] percentage HSL

    const attemptHSL = stats.successes.map(e => Math.floor((e/5000)*120)); // [0, 5000] achievement
    const successHSL = stats.successes.map((e, i) => Math.floor((e/stats.attempts[i])*120)); // relative success (success/attempts)

    const tableRow = new Array(10).fill(0).map((e, i) =>
`<tr>
<td><p class="card-text">${jobNames[i]}</p></td>
<td><p class="card-text"><span style="color: hsl(${prestHSL[i] || 0}, 67%, 50%);">P${jobs.prestiges[i] || 0}</span></p></td>
<td><p class="card-text"><span style="color: hsl(${percHSL[i] || 0}, 67%, 50%);">${jobs.percentages[i] || 0}%</span></p></td>
<td><p class="card-text"><span style="color: hsl(${attemptHSL[i] || 0}, 67%, 50%);">${stats.attempts[i] || 0}</span></p></td>
<td><p class="card-text"><span style="color: hsl(${successHSL[i] || 0}, 67%, 50%);">${stats.successes[i] || 0}</span></p></td>
</tr>`);

    return `<h3>Job Progress</h3>
<div class="card border-0">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">Job</th>
<th scope="col">Prestige</th>
<th scope="col">Percentage</th>
<th scope="col">Attempts</th>
<th scope="col">Successes</th>
</tr>
</thead>
<tbody>
${tableRow.join('')}
</tbody>
</table>
<p>&nbsp;</p>
<p class="card-text">Last updated: ${new Date(Math.min(jobs.last_updated, stats.last_updated)).toGMTString()}</p>
</div>
</div>`;
}