CE autoSig development

Add dynamic content to your profile signature

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