您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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> </p> <p class="card-text">Last updated: ${new Date(Math.min(jobs.last_updated, stats.last_updated)).toGMTString()}</p> </div> </div>`; }