CE autoSig development

Add dynamic content to your profile signature

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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